diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 841ec9e8..8ba7f572 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,23 +1,12 @@ -name: MT5 Helm GitOps Deploy +name: MT5 Stack Deploy on: push: branches: [ "main" ] - tags: [ "*" ] workflow_dispatch: -env: - # Gleiche Registry wie im boday_tf-Projekt, zwei Namen: - # PUSH_REGISTRY: wie der Runner sie erreicht - # PULL_REGISTRY: wie k3s/containerd sie erreicht (registries.yaml, CA unter certs.d) - PUSH_REGISTRY: localhost:3005 - PULL_REGISTRY: registry.local - # Host-Header für Traefik-Ingress des Web-UI (leer = kein Ingress, kein Health-Check per HTTP) - INGRESS_HOST: mt5.local - jobs: - build-and-deploy: - if: false + deploy: runs-on: ubuntu-latest container: catthehacker/ubuntu:act-latest steps: @@ -26,87 +15,21 @@ jobs: # HINWEIS: Terraform-Binary kommt wie bei boday_tf über das globale Runner-Volume (:ro) - - name: Trust Local Nginx Certificate Inside Container + # Nur versionierte Dateien deployen (kein .git, keine Secrets, keine Laufzeitdaten) + - name: Build deploy bundle run: | - mkdir -p /usr/local/share/ca-certificates/ - cp nginx_ssl/nginx.crt /usr/local/share/ca-certificates/nginx-sandbox.crt 2>/dev/null || echo "Nutze Workspace-Zertifikat" - update-ca-certificates + rm -rf .deploy-bundle && mkdir .deploy-bundle + git archive HEAD | tar -x -C .deploy-bundle + if find .deploy-bundle -name bots.env -o -name .env -o -name '*.session' | grep -q .; then + echo "ABBRUCH: bots.env / .env / *.session ist im Repo versioniert"; exit 1 + fi + du -sh .deploy-bundle - # Docker verlangt Image-Namen in Kleinbuchstaben – "MetaTrader5-Docker-Image" würde sonst scheitern - - name: Compute Image Name - run: | - IMAGE_PATH="$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" - echo "IMAGE=${PUSH_REGISTRY}/${IMAGE_PATH}" >> "$GITHUB_ENV" - echo "PULL_IMAGE=${PULL_REGISTRY}/${IMAGE_PATH}" >> "$GITHUB_ENV" - echo "Push: ${PUSH_REGISTRY}/${IMAGE_PATH}" - echo "Pull: ${PULL_REGISTRY}/${IMAGE_PATH}" - - - name: Log in to Local Registry - run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${PUSH_REGISTRY} -u ${{ github.actor }} --password-stdin - - # Ein Build, zwei Tags (das Image ist ~4 GB – doppelt bauen kostet unnötig Zeit) - - name: Build MT5 Image - run: | - docker build --platform linux/amd64 \ - -t ${IMAGE}:${{ github.sha }} \ - -t ${IMAGE}:latest \ - . - - - name: Push MT5 Image - run: | - docker push ${IMAGE}:${{ github.sha }} - docker push ${IMAGE}:latest - - - name: Prepare Kubernetes Access - run: | - echo "${{ secrets.KUBECONFIG_DATA }}" > ./kind_kubeconfig - chmod 600 ./kind_kubeconfig - - # Erster Rollout kann bis 15 Min dauern (Wine + MT5 Installation nach /config) - # init mit Retry, weil der DNS-Resolver sporadisch "server misbehaving" liefert - name: Terraform Deploy env: - # Web-UI-Login (KasmVNC) – wie bisher - TF_VAR_vnc_user: ${{ secrets.MT5_VNC_USER || 'trader' }} - TF_VAR_vnc_password: ${{ secrets.MT5_VNC_PASSWORD }} - # Broker-Login für MT5 – landet als login.ini im Pod und wird beim Start geladen - TF_VAR_mt5_login: ${{ secrets.MT5USER }} - TF_VAR_mt5_password: ${{ secrets.MT5PASSWORD }} - TF_VAR_mt5_server: ${{ secrets.MT5_SERVER }} + TF_VAR_ssh_private_key: ${{ secrets.DEPLOY_SSH_KEY }} + TF_VAR_ssh_host: ${{ vars.DEPLOY_HOST || 'BITMT503' }} run: | terraform --version - for i in 1 2 3; do - terraform init && break - if [ "$i" = "3" ]; then echo "terraform init nach 3 Versuchen fehlgeschlagen"; exit 1; fi - echo "init fehlgeschlagen, Versuch $i – neuer Anlauf in 10s" - sleep 10 - done - terraform apply -auto-approve \ - -var="image_repository=${PULL_IMAGE}" \ - -var="app_version=${{ github.sha }}" \ - -var="ingress_host=${INGRESS_HOST}" - - # KasmVNC antwortet ohne Login mit 401 – daher mit Basic Auth prüfen und auf den Erststart warten - - name: Verify App Health - if: env.INGRESS_HOST != '' - env: - VNC_USER: ${{ secrets.MT5_VNC_USER || 'trader' }} - VNC_PASS: ${{ secrets.MT5_VNC_PASSWORD }} - run: | - for i in $(seq 1 40); do - CODE=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 \ - -u "${VNC_USER}:${VNC_PASS}" -H "Host: ${INGRESS_HOST}" \ - http://local-k3s-platform:80/ || true) - echo "Versuch ${i}: HTTP ${CODE}" - if [ "${CODE}" = "200" ]; then - echo "Erfolg: MetaTrader 5 Web-UI läuft via Helm im GitOps-Standard!" - exit 0 - fi - sleep 15 - done - echo "MT5 Web-UI nach 10 Minuten nicht erreichbar" - exit 1 - - - name: Cleanup Kubeconfig - if: always() - run: rm -f ./kind_kubeconfig + for i in 1 2 3; do terraform init && break; echo "init fehlgeschlagen, Versuch $i"; sleep 10; done + terraform apply -auto-approve diff --git a/.github/workflows/deploy.yml.k3s b/.github/workflows/deploy.yml.k3s new file mode 100644 index 00000000..841ec9e8 --- /dev/null +++ b/.github/workflows/deploy.yml.k3s @@ -0,0 +1,112 @@ +name: MT5 Helm GitOps Deploy + +on: + push: + branches: [ "main" ] + tags: [ "*" ] + workflow_dispatch: + +env: + # Gleiche Registry wie im boday_tf-Projekt, zwei Namen: + # PUSH_REGISTRY: wie der Runner sie erreicht + # PULL_REGISTRY: wie k3s/containerd sie erreicht (registries.yaml, CA unter certs.d) + PUSH_REGISTRY: localhost:3005 + PULL_REGISTRY: registry.local + # Host-Header für Traefik-Ingress des Web-UI (leer = kein Ingress, kein Health-Check per HTTP) + INGRESS_HOST: mt5.local + +jobs: + build-and-deploy: + if: false + runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + # HINWEIS: Terraform-Binary kommt wie bei boday_tf über das globale Runner-Volume (:ro) + + - name: Trust Local Nginx Certificate Inside Container + run: | + mkdir -p /usr/local/share/ca-certificates/ + cp nginx_ssl/nginx.crt /usr/local/share/ca-certificates/nginx-sandbox.crt 2>/dev/null || echo "Nutze Workspace-Zertifikat" + update-ca-certificates + + # Docker verlangt Image-Namen in Kleinbuchstaben – "MetaTrader5-Docker-Image" würde sonst scheitern + - name: Compute Image Name + run: | + IMAGE_PATH="$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" + echo "IMAGE=${PUSH_REGISTRY}/${IMAGE_PATH}" >> "$GITHUB_ENV" + echo "PULL_IMAGE=${PULL_REGISTRY}/${IMAGE_PATH}" >> "$GITHUB_ENV" + echo "Push: ${PUSH_REGISTRY}/${IMAGE_PATH}" + echo "Pull: ${PULL_REGISTRY}/${IMAGE_PATH}" + + - name: Log in to Local Registry + run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${PUSH_REGISTRY} -u ${{ github.actor }} --password-stdin + + # Ein Build, zwei Tags (das Image ist ~4 GB – doppelt bauen kostet unnötig Zeit) + - name: Build MT5 Image + run: | + docker build --platform linux/amd64 \ + -t ${IMAGE}:${{ github.sha }} \ + -t ${IMAGE}:latest \ + . + + - name: Push MT5 Image + run: | + docker push ${IMAGE}:${{ github.sha }} + docker push ${IMAGE}:latest + + - name: Prepare Kubernetes Access + run: | + echo "${{ secrets.KUBECONFIG_DATA }}" > ./kind_kubeconfig + chmod 600 ./kind_kubeconfig + + # Erster Rollout kann bis 15 Min dauern (Wine + MT5 Installation nach /config) + # init mit Retry, weil der DNS-Resolver sporadisch "server misbehaving" liefert + - name: Terraform Deploy + env: + # Web-UI-Login (KasmVNC) – wie bisher + TF_VAR_vnc_user: ${{ secrets.MT5_VNC_USER || 'trader' }} + TF_VAR_vnc_password: ${{ secrets.MT5_VNC_PASSWORD }} + # Broker-Login für MT5 – landet als login.ini im Pod und wird beim Start geladen + TF_VAR_mt5_login: ${{ secrets.MT5USER }} + TF_VAR_mt5_password: ${{ secrets.MT5PASSWORD }} + TF_VAR_mt5_server: ${{ secrets.MT5_SERVER }} + run: | + terraform --version + for i in 1 2 3; do + terraform init && break + if [ "$i" = "3" ]; then echo "terraform init nach 3 Versuchen fehlgeschlagen"; exit 1; fi + echo "init fehlgeschlagen, Versuch $i – neuer Anlauf in 10s" + sleep 10 + done + terraform apply -auto-approve \ + -var="image_repository=${PULL_IMAGE}" \ + -var="app_version=${{ github.sha }}" \ + -var="ingress_host=${INGRESS_HOST}" + + # KasmVNC antwortet ohne Login mit 401 – daher mit Basic Auth prüfen und auf den Erststart warten + - name: Verify App Health + if: env.INGRESS_HOST != '' + env: + VNC_USER: ${{ secrets.MT5_VNC_USER || 'trader' }} + VNC_PASS: ${{ secrets.MT5_VNC_PASSWORD }} + run: | + for i in $(seq 1 40); do + CODE=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 \ + -u "${VNC_USER}:${VNC_PASS}" -H "Host: ${INGRESS_HOST}" \ + http://local-k3s-platform:80/ || true) + echo "Versuch ${i}: HTTP ${CODE}" + if [ "${CODE}" = "200" ]; then + echo "Erfolg: MetaTrader 5 Web-UI läuft via Helm im GitOps-Standard!" + exit 0 + fi + sleep 15 + done + echo "MT5 Web-UI nach 10 Minuten nicht erreichbar" + exit 1 + + - name: Cleanup Kubeconfig + if: always() + run: rm -f ./kind_kubeconfig diff --git a/.gitignore b/.gitignore index 380a1908..9c0f67fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,20 @@ trader-data config .env + +# --- Deploy: Secrets und Laufzeitdaten bleiben nur auf dem Server --- +bots.env +.env +*.session +config/ +config.*/ +trader-data/ +# Terraform +.terraform/ +.terraform.lock.hcl +*.tfstate +*.tfstate.* +.deploy-bundle/ +__pycache__/ +*.bak +*.bak-* diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 00000000..cedba8af --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,39 @@ +# Deploy dieses Stacks + +Diese App deployt sich selbst – unabhängig von anderen Apps. + +| Datei | Zweck | +|---|---| +| `deploy.conf` | Kenndaten: Dienstnamen, MCP-Port, Diagnosedatei | +| `main.tf` | Terraform: Sperre → Dateien per SSH kopieren → `scripts/deploy-local.sh` | +| `.github/workflows/deploy.yml` | Forgejo: Bundle mit `git archive` bauen, `terraform apply` | +| `scripts/deploy-local.sh` | Deploy auf dem Server: Sperre, `docker compose up -d --build`, warten | +| `scripts/check-stack.sh` | Statusprüfung: Container, Bridge, Konto, Kurs, Positionen, Trader, Telegram | + +## Deployen + +| Weg | Befehl | +|---|---| +| Push auf `main` | Forgejo-Workflow → Terraform | +| Forgejo manuell | Actions → *MT5 Stack Deploy* → Run workflow | +| Direkt auf dem Server | `cd && scripts/deploy-local.sh` | + +**Sperre:** Ein Deploy bricht ab, solange der Stack offene Positionen hat. Der Trader +hält Positionen nur im Speicher, nach einem Neustart setzt er für sie kein BE mehr. +`scripts/deploy-local.sh --force` übergeht die Sperre bewusst. + +**Nur auf dem Server, nie im Repo:** `config/`, `trader-data/`, `.env`, `bots.env`. +Terraform kopiert nur versionierte Dateien (`git archive`), diese bleiben unberührt. + +**Direkt auf dem Server geändert?** Dort committen und pushen – sonst dreht der +nächste Deploy die Änderung zurück. + +## Status +```bash +scripts/check-stack.sh # ausführlich +scripts/check-stack.sh -q # nur Fehler (cron) +``` + +## Forgejo einrichten +- Secret `DEPLOY_SSH_KEY`: privater SSH-Key für `bast@BITMT503` +- optional Variable `DEPLOY_HOST`: IP, falls der Runner `BITMT503` nicht auflöst diff --git a/deploy.conf b/deploy.conf new file mode 100644 index 00000000..c9c08f13 --- /dev/null +++ b/deploy.conf @@ -0,0 +1,8 @@ +# deploy.conf – Kenndaten DIESES Stacks (von scripts/*.sh gelesen) +STACK_KEY=hm +STACK_NAME="HM (ch2, PU Prime)" +MCP_PORT=8000 # Host-Port aus docker-compose.yaml (127.0.0.1:8000:8000) +MT5_SVC=mt5 # Dienstnamen aus docker-compose.yaml +MCP_SVC=mt5-mcp +TRADER_SVC=live-trader +DIAG_FILE=trader-data/diagnostics_ch2-HM.json diff --git a/destroy-tf-force.sh b/destroy-tf-force.sh.k3s similarity index 100% rename from destroy-tf-force.sh rename to destroy-tf-force.sh.k3s diff --git a/main.tf b/main.tf index 8aa4cf11..bc469723 100644 --- a/main.tf +++ b/main.tf @@ -1,105 +1,72 @@ -terraform { - required_providers { - kubernetes = { - source = "hashicorp/kubernetes" - version = "~> 2.26" - } - helm = { - source = "hashicorp/helm" - version = "~> 2.12" - } - } +# Deployt DIESEN Stack per SSH auf den Server – unabhängig von anderen Apps. +# Ablauf: Sperre prüfen -> sauberes Bundle (git archive) in den Stack-Ordner +# kopieren -> scripts/deploy-local.sh (docker compose up -d --build + Check). +# config/, trader-data/, .env und bots.env auf dem Server bleiben unberührt, +# weil sie nicht im Repo (und damit nicht im Bundle) sind. - # Eigener State-Pfad, damit sich MT5 und die Webseite nicht in die Quere kommen +terraform { backend "consul" { address = "consul:8500" - path = "terraform/states/mt5_tf" + path = "terraform/states/mt5_hm" # pro Repo eindeutig } } -# --- Variablen --------------------------------------------------------------- - -variable "app_version" { +variable "ssh_host" { type = string - default = "latest" + default = "BITMT503" } -variable "image_repository" { +variable "ssh_user" { type = string - default = "gmag11/metatrader5_vnc" + default = "bast" } -variable "vnc_user" { - type = string - default = "trader" -} - -# Kommt aus TF_VAR_vnc_password (Secret MT5_VNC_PASSWORD) -variable "vnc_password" { +variable "ssh_private_key" { type = string sensitive = true } -variable "ingress_host" { +variable "stack_dir" { type = string - default = "" + default = "/home/bast/build/MetaTrader5-Docker-Image" } -# --- Provider ---------------------------------------------------------------- - -provider "kubernetes" { - config_path = "./kind_kubeconfig" - host = "https://local-k3s-platform:6443" - insecure = true +# Wird vom Workflow per "git archive HEAD" erzeugt – nur versionierte Dateien +variable "bundle_dir" { + type = string + default = ".deploy-bundle" } -provider "helm" { - kubernetes { - config_path = "./kind_kubeconfig" - host = "https://local-k3s-platform:6443" - insecure = true - } -} - -# --- MetaTrader 5 Deployment ------------------------------------------------- - -resource "helm_release" "mt5" { - name = "mt5" - chart = "./charts/mt5" - namespace = "trading" - create_namespace = true - - # Erster Start installiert Wine + MT5 nach /config – das dauert ein paar Minuten - timeout = 900 - wait = true - - set { - name = "image.repository" - value = var.image_repository - } - - set { - name = "image.tag" - value = var.app_version - } - - set { - name = "auth.user" - value = var.vnc_user - } - - set_sensitive { - name = "auth.password" - value = var.vnc_password - } - - set { - name = "ingress.enabled" - value = var.ingress_host != "" ? "true" : "false" - } - - set { - name = "ingress.host" - value = var.ingress_host +resource "terraform_data" "stack" { + # Neu deployen nur, wenn sich eine versionierte Datei geändert hat + triggers_replace = sha1(join("", [ + for f in sort(fileset(var.bundle_dir, "**")) : "${f}:${filesha1("${var.bundle_dir}/${f}")}" + ])) + + connection { + type = "ssh" + host = var.ssh_host + user = var.ssh_user + private_key = var.ssh_private_key + } + + # 1. Sperre mit dem Script, das schon auf dem Server liegt (beim ersten Mal fehlt es) + provisioner "remote-exec" { + inline = [ + "cd '${var.stack_dir}' && if [ -x scripts/deploy-local.sh ]; then scripts/deploy-local.sh --check-flat; else echo 'Erster Deploy – keine Sperre'; fi", + ] + } + + # 2. Versionierte Dateien in den Stack-Ordner kopieren + provisioner "file" { + source = "${var.bundle_dir}/" + destination = var.stack_dir + } + + # 3. Bauen, starten, auf gesunden Stack warten + provisioner "remote-exec" { + inline = [ + "cd '${var.stack_dir}' && chmod +x scripts/*.sh && scripts/deploy-local.sh", + ] } } diff --git a/main.tf.k3s b/main.tf.k3s new file mode 100644 index 00000000..8aa4cf11 --- /dev/null +++ b/main.tf.k3s @@ -0,0 +1,105 @@ +terraform { + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.26" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.12" + } + } + + # Eigener State-Pfad, damit sich MT5 und die Webseite nicht in die Quere kommen + backend "consul" { + address = "consul:8500" + path = "terraform/states/mt5_tf" + } +} + +# --- Variablen --------------------------------------------------------------- + +variable "app_version" { + type = string + default = "latest" +} + +variable "image_repository" { + type = string + default = "gmag11/metatrader5_vnc" +} + +variable "vnc_user" { + type = string + default = "trader" +} + +# Kommt aus TF_VAR_vnc_password (Secret MT5_VNC_PASSWORD) +variable "vnc_password" { + type = string + sensitive = true +} + +variable "ingress_host" { + type = string + default = "" +} + +# --- Provider ---------------------------------------------------------------- + +provider "kubernetes" { + config_path = "./kind_kubeconfig" + host = "https://local-k3s-platform:6443" + insecure = true +} + +provider "helm" { + kubernetes { + config_path = "./kind_kubeconfig" + host = "https://local-k3s-platform:6443" + insecure = true + } +} + +# --- MetaTrader 5 Deployment ------------------------------------------------- + +resource "helm_release" "mt5" { + name = "mt5" + chart = "./charts/mt5" + namespace = "trading" + create_namespace = true + + # Erster Start installiert Wine + MT5 nach /config – das dauert ein paar Minuten + timeout = 900 + wait = true + + set { + name = "image.repository" + value = var.image_repository + } + + set { + name = "image.tag" + value = var.app_version + } + + set { + name = "auth.user" + value = var.vnc_user + } + + set_sensitive { + name = "auth.password" + value = var.vnc_password + } + + set { + name = "ingress.enabled" + value = var.ingress_host != "" ? "true" : "false" + } + + set { + name = "ingress.host" + value = var.ingress_host + } +} diff --git a/mt5-stack.zip b/mt5-stack.zip deleted file mode 100644 index 31a522bb..00000000 Binary files a/mt5-stack.zip and /dev/null differ diff --git a/scripts/check-stack.sh b/scripts/check-stack.sh new file mode 100755 index 00000000..dc47886d --- /dev/null +++ b/scripts/check-stack.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# check-stack.sh – Statusprüfung DIESES Stacks (MT5, MCP, Trader, Telegram) +# Läuft im Stack-Ordner auf dem Server; Werte kommen aus deploy.conf. +# +# scripts/check-stack.sh ausführlich +# scripts/check-stack.sh -q nur Fehler/Warnungen (cron/Deploy) +# Exit-Code: 0 = ok, 1 = mindestens ein Fehler (Warnungen zählen nicht) + +cd "$(dirname "$0")/.." || exit 1 +source ./deploy.conf + +QUIET=0; [ "${1:-}" = "-q" ] && QUIET=1 +FAIL=0 +SYMBOL="${CHECK_SYMBOL:-XAUUSD}" +BASE="http://127.0.0.1:$MCP_PORT" + +if [ -t 1 ]; then G=$'\e[32m'; R=$'\e[31m'; Y=$'\e[33m'; B=$'\e[1m'; N=$'\e[0m'; else G=; R=; Y=; B=; N=; fi +pfx() { [ $QUIET -eq 1 ] && echo "[$STACK_KEY] "; } +ok() { [ $QUIET -eq 1 ] || echo " ${G}OK${N} $*"; } +warn() { echo " ${Y}WARN${N} $(pfx)$*"; } +fail() { echo " ${R}FAIL${N} $(pfx)$*"; FAIL=1; } + +jget() { python3 -c "import json,sys +try: v=json.load(sys.stdin) +except Exception: print('${2:-}'); sys.exit() +for k in '$1'.split('.'): + v = v.get(k) if isinstance(v, dict) else None +print('${2:-}' if v is None else v)"; } + +cid() { docker compose ps -q "$1" 2>/dev/null | head -1; } + +[ $QUIET -eq 1 ] || echo "${B}== $STACK_NAME ==${N}" + +# 1. Container +for svc in "$MT5_SVC" "$MCP_SVC" "$TRADER_SVC"; do + id=$(cid "$svc") + if [ -z "$id" ]; then fail "Dienst $svc: kein Container"; continue; fi + st=$(docker inspect -f '{{.State.Status}}{{if .State.Health}}/{{.State.Health.Status}}{{end}}' "$id") + case "$st" in running/healthy|running) ok "Dienst $svc: $st" ;; *) fail "Dienst $svc: $st" ;; esac +done + +# 2. Bridge + Konto +H=$(curl -s -m 10 "$BASE/health") +[ "$(echo "$H" | jget connected)" = "True" ] && ok "MCP $BASE: Bridge verbunden" \ + || fail "MCP $BASE: keine Verbindung zu MT5 (${H:-keine Antwort})" + +A=$(curl -s -m 15 "$BASE/account") +if [ -n "$(echo "$A" | jget login)" ]; then + ok "Konto $(echo "$A" | jget login) @ $(echo "$A" | jget server): Balance $(echo "$A" | jget balance)" \ + "Equity $(echo "$A" | jget equity) $(echo "$A" | jget currency)" +else + fail "Konto: $(echo "$A" | jget detail "${A:-keine Antwort}")" +fi + +# 3. Kurs +T=$(curl -s -m 15 "$BASE/tick/$SYMBOL") +if [ -n "$(echo "$T" | jget bid)" ]; then + ok "$SYMBOL → $(echo "$T" | jget symbol): bid $(echo "$T" | jget bid) ask $(echo "$T" | jget ask)" +else + fail "Kurs $SYMBOL: $(echo "$T" | jget detail "${T:-keine Antwort}")" +fi + +# 4. Positionen +P=$(curl -s -m 15 "$BASE/positions" | jget count) +if [ -z "$P" ]; then fail "Positionen: keine Antwort" +elif [ "$P" -gt 0 ]; then warn "Offene Positionen: $P" +else ok "Offene Positionen: 0"; fi + +# 5. Trader-Lebenszeichen (Docker-Zeitstempel = UTC, unabhängig von Zeitzonen) +TID=$(cid "$TRADER_SVC") +if [ -n "$TID" ]; then + LAST=$(docker logs --timestamps --since 10m "$TID" 2>&1 | grep -E "\[(LIVE|DRY)\] Balance" | tail -1) + if [ -n "$LAST" ]; then + AGE=$(( $(date +%s) - $(date -d "$(echo "$LAST" | awk '{print $1}')" +%s 2>/dev/null || echo 0) )) + MODE=$(echo "$LAST" | grep -oE "LIVE|DRY" | head -1) + [ "$AGE" -le 120 ] && ok "Trader lebt ($MODE), letztes Lebenszeichen vor ${AGE}s" \ + || fail "Trader: letztes Lebenszeichen vor ${AGE}s" + else + fail "Trader: keine Balance-Zeile in den letzten 10 min" + fi + + # 6. Telegram + START=$(docker inspect -f '{{.State.StartedAt}}' "$TID") + CHAN=$(docker logs --since "$START" "$TID" 2>&1 | grep -oE "Active channels: [^|]*" | tail -1) + [ -n "$CHAN" ] && ok "Telegram: $CHAN" || fail "Telegram: keine 'Active channels'-Meldung seit Start" + ERRS=$(docker logs --since 60m "$TID" 2>&1 \ + | grep -ciE "AUTH_KEY|EOFError|SessionPasswordNeeded|PhoneNumber|Traceback|disconnected|server closed") + [ "$ERRS" -gt 0 ] && warn "Telegram/Trader: $ERRS Fehlerzeile(n) in der letzten Stunde" +fi + +if [ -f "$DIAG_FILE" ]; then + DAGE=$(( $(date +%s) - $(stat -c %Y "$DIAG_FILE") )) + [ "$DAGE" -le 900 ] && ok "Diagnose: vor ${DAGE}s geschrieben, $(jget total_messages < "$DIAG_FILE") Nachricht(en)" \ + || fail "Diagnose: seit ${DAGE}s nicht geschrieben ($DIAG_FILE)" +else + warn "Diagnose: $DIAG_FILE fehlt" +fi + +[ $QUIET -eq 1 ] || { [ $FAIL -eq 0 ] && echo "${G}${B}Alles in Ordnung.${N}" || echo "${R}${B}Fehler – siehe oben.${N}"; } +exit $FAIL diff --git a/scripts/deploy-local.sh b/scripts/deploy-local.sh new file mode 100755 index 00000000..9b784470 --- /dev/null +++ b/scripts/deploy-local.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# deploy-local.sh – Deploy DIESES Stacks, ausgeführt im Stack-Ordner auf dem Server. +# Wird von Terraform aufgerufen, funktioniert aber genauso von Hand: +# +# scripts/deploy-local.sh Sperre prüfen, bauen/starten, auf gesund warten +# scripts/deploy-local.sh --check-flat nur die Sperre prüfen (Terraform vor dem Kopieren) +# scripts/deploy-local.sh --force Sperre ignorieren +# +# Sperre: Der Trader hält offene Positionen nur im Speicher. Ein Neustart mitten +# im Trade = für diese Position kein BE und kein CANCEL mehr. + +cd "$(dirname "$0")/.." || exit 1 +source ./deploy.conf + +check_flat() { + local n + n=$(curl -s -m 10 "http://127.0.0.1:$MCP_PORT/positions" \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["count"])' 2>/dev/null || echo "?") + if [ "$n" = "0" ]; then + echo "[$STACK_KEY] Keine offenen Positionen – Deploy erlaubt." + return 0 + fi + # Stack (noch) nicht gestartet -> nichts, was verloren gehen kann + if [ "$n" = "?" ] && [ -z "$(docker compose ps -q "$TRADER_SVC" 2>/dev/null)" ]; then + echo "[$STACK_KEY] Trader läuft nicht – Deploy erlaubt." + return 0 + fi + echo "[$STACK_KEY] ABBRUCH: $n offene Position(en) bzw. MCP nicht erreichbar." + echo " Warten bis flat, oder bewusst: scripts/deploy-local.sh --force" + return 1 +} + +case "${1:-}" in + --check-flat) check_flat; exit $? ;; + --force) echo "[$STACK_KEY] --force: Sperre übersprungen" ;; + *) check_flat || exit 1 ;; +esac + +chmod +x scripts/*.sh 2>/dev/null +echo "[$STACK_KEY] docker compose up -d --build" +docker compose up -d --build || exit 1 + +echo "[$STACK_KEY] Warte auf gesunden Stack (max. 10 min)..." +for i in $(seq 1 20); do + if scripts/check-stack.sh -q >/dev/null 2>&1; then + scripts/check-stack.sh + exit 0 + fi + sleep 30 +done +scripts/check-stack.sh -q +exit 1