Initial PlayBull release with Trilogy Hub integration.

Includes homelab deploy tooling, image position/zoom manifest persistence, and full trading/casino stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-02 08:31:36 +02:00
commit 94b6cc3be9
60 changed files with 9206 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
# Trilogy Hub — Caddy-Block (Homelab: progress-caddy)
# Datei auf dem Server: /home/gizzler/pgs2/finalprogressharing/Caddyfile
# Nach Aenderung: docker exec progress-caddy caddy reload --config /etc/caddy/Caddyfile
trilogyhub.de, www.trilogyhub.de {
encode gzip zstd
@ws {
header Connection *Upgrade*
header Upgrade websocket
}
reverse_proxy 192.168.178.49:3080
}
halmc.duckdns.org {
encode gzip zstd
@ws {
header Connection *Upgrade*
header Upgrade websocket
}
reverse_proxy 192.168.178.49:3080
}

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Cloudflare Dynamic DNS — aktualisiert A/AAAA wenn sich die Fritz!Box-WAN-IP aendert.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${SCRIPT_DIR}/env.homelab"
STATE_FILE="${SCRIPT_DIR}/.cf-ddns-last-ip"
[[ -f "${ENV_FILE}" ]] || { echo "cloudflare-ddns: ${ENV_FILE} fehlt" >&2; exit 1; }
# shellcheck source=/dev/null
source <(sed 's/\r$//' "${ENV_FILE}")
: "${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN fehlt}"
: "${CLOUDFLARE_ZONE_ID:?CLOUDFLARE_ZONE_ID fehlt}"
APP_DOMAIN="${APP_DOMAIN:-trilogyhub.de}"
DDNS_RECORD="${DDNS_RECORD_NAME:-home.${APP_DOMAIN}}"
DDNS_PROXIED="${DDNS_PROXIED:-true}"
IPV4="$(curl -4 -fsS --max-time 15 https://api.ipify.org 2>/dev/null || true)"
IPV6="$(curl -6 -fsS --max-time 15 https://api64.ipify.org 2>/dev/null || true)"
[[ -n "${IPV4}" || -n "${IPV6}" ]] || { echo "cloudflare-ddns: keine IP" >&2; exit 1; }
CURRENT="${IPV4}|${IPV6}"
LAST="$(cat "${STATE_FILE}" 2>/dev/null || true)"
if [[ "${CURRENT}" == "${LAST}" ]]; then
echo "$(date -Is) cloudflare-ddns ${DDNS_RECORD}: unveraendert v4=${IPV4:-none}"
exit 0
fi
export CF_API="https://api.cloudflare.com/client/v4"
export CF_ZONE="${CLOUDFLARE_ZONE_ID}"
export CF_TOKEN="${CLOUDFLARE_API_TOKEN}"
export CF_PROXIED="${DDNS_PROXIED}"
upsert() {
local rtype="$1" ip="$2"
[[ -n "${ip}" ]] || return 0
python3 - "${DDNS_RECORD}" "${rtype}" "${ip}" <<'PY'
import json, os, sys, urllib.request
name, rtype, ip = sys.argv[1:4]
api = os.environ["CF_API"]
zone = os.environ["CF_ZONE"]
token = os.environ["CF_TOKEN"]
proxied = os.environ.get("CF_PROXIED", "true").lower() == "true"
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
list_url = f"{api}/zones/{zone}/dns_records?type={rtype}&name={name}"
with urllib.request.urlopen(urllib.request.Request(list_url, headers=headers), timeout=20) as r:
data = json.load(r)
if not data.get("success"):
raise SystemExit(f"list failed: {data}")
body = json.dumps({"type": rtype, "name": name, "content": ip, "ttl": 120, "proxied": proxied}).encode()
recs = data.get("result", [])
if recs:
url = f"{api}/zones/{zone}/dns_records/{recs[0]['id']}"
req = urllib.request.Request(url, data=body, headers=headers, method="PUT")
act = "aktualisiert"
else:
url = f"{api}/zones/{zone}/dns_records"
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
act = "angelegt"
with urllib.request.urlopen(req, timeout=20) as r:
out = json.load(r)
if not out.get("success"):
raise SystemExit(f"write failed: {out}")
print(f"{rtype} {name} -> {ip} ({act})")
PY
}
upsert A "${IPV4}"
upsert AAAA "${IPV6}"
echo "${CURRENT}" > "${STATE_FILE}"
chmod 600 "${STATE_FILE}" 2>/dev/null || true
echo "$(date -Is) cloudflare-ddns fertig"

View File

@@ -0,0 +1,26 @@
# Cloudflare Tunnel — HTTPS am Edge, kein Port-Forwarding noetig.
# Token wird per systemd EnvironmentFile gesetzt (deploy/env.homelab).
#
# Zero Trust Dashboard:
# Networks → Tunnels → Create → Cloudflared
# Public Hostname: trilogyhub.de → http://127.0.0.1:3080
# Tunnel-Token in CLOUDFLARE_TUNNEL_TOKEN eintragen
#
# Hinweis: Hostnames im Zero-Trust-Dashboard eintragen (Token-Install).
# Cloudflare-DDNS meldet WAN-IP fuer home.trilogyhub.de (optional).
tunnel: playbull
credentials-file: /etc/cloudflared/playbull.json
ingress:
- hostname: trilogyhub.de
service: http://127.0.0.1:3080
originRequest:
noTLSVerify: true
httpHostHeader: trilogyhub.de
- hostname: www.trilogyhub.de
service: http://127.0.0.1:3080
originRequest:
noTLSVerify: true
httpHostHeader: www.trilogyhub.de
- service: http_status:404

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env bash
# trilogyhub.de mit Homelab-Website verbinden (Nginx + optional Certbot + Tunnel-Hinweis)
set -euo pipefail
DOMAIN="${1:-trilogyhub.de}"
PORT="${APP_PORT:-3080}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
ENV_FILE="${APP_DIR}/deploy/env.homelab"
if [[ "${EUID}" -ne 0 ]]; then
echo "sudo bash deploy/connect-trilogyhub.sh" >&2
exit 1
fi
[[ -f "${ENV_FILE}" ]] || { echo "${ENV_FILE} fehlt" >&2; exit 1; }
sed -i 's/\r$//' "${ENV_FILE}" 2>/dev/null || true
# shellcheck source=/dev/null
source "${ENV_FILE}"
PORT="${APP_PORT:-3080}"
DOMAIN="${APP_DOMAIN:-${DOMAIN}}"
echo "==> Verbinde ${DOMAIN} → 127.0.0.1:${PORT}"
# env aktualisieren
grep -q '^APP_DOMAIN=' "${ENV_FILE}" && sed -i "s|^APP_DOMAIN=.*|APP_DOMAIN=${DOMAIN}|" "${ENV_FILE}" || echo "APP_DOMAIN=${DOMAIN}" >> "${ENV_FILE}"
# LAN (Fritz-Netz)
sed -i 's/\r$//' "${APP_DIR}/deploy/nginx/bestewebsite-lan.conf" 2>/dev/null || true
cp "${APP_DIR}/deploy/nginx/trilogyhub.conf" /etc/nginx/sites-available/trilogyhub 2>/dev/null || \
cp "${APP_DIR}/deploy/nginx/bestewebsite.conf" /etc/nginx/sites-available/trilogyhub
# HTTP-only zuerst (fuer Certbot)
cat > /etc/nginx/sites-available/trilogyhub <<EOF
server {
listen 80;
listen [::]:80;
server_name ${DOMAIN} www.${DOMAIN};
client_max_body_size 30m;
location / {
proxy_pass http://127.0.0.1:${PORT};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 86400;
}
}
EOF
if [[ -f "${APP_DIR}/deploy/nginx/bestewebsite-lan.conf" ]]; then
cp "${APP_DIR}/deploy/nginx/bestewebsite-lan.conf" /etc/nginx/sites-available/bestewebsite-lan
ln -sf /etc/nginx/sites-available/bestewebsite-lan /etc/nginx/sites-enabled/00-bestewebsite-lan
fi
ln -sf /etc/nginx/sites-available/trilogyhub /etc/nginx/sites-enabled/trilogyhub
# alte DuckDNS-Sites nicht loeschen — nur Redirect optional
if [[ -f /etc/nginx/sites-available/bestewebsite ]]; then
cat > /etc/nginx/sites-available/bestewebsite-legacy <<'LEG'
server {
listen 80;
listen [::]:80;
server_name halmc.duckdns.org pimmelschwanz.duckdns.org;
return 301 https://trilogyhub.de$request_uri;
}
LEG
ln -sf /etc/nginx/sites-available/bestewebsite-legacy /etc/nginx/sites-enabled/bestewebsite-legacy 2>/dev/null || true
fi
nginx -t
systemctl reload nginx
# App muss laufen
if ! curl -sf "http://127.0.0.1:${PORT}/" >/dev/null; then
echo "WARNUNG: nichts auf Port ${PORT} — playbull.service starten" >&2
systemctl restart playbull 2>/dev/null || true
fi
# Certbot wenn DNS auf diesen Server zeigt
if command -v certbot >/dev/null 2>&1; then
echo "==> Versuche Let's Encrypt fuer ${DOMAIN}..."
if certbot --nginx -d "${DOMAIN}" -d "www.${DOMAIN}" \
--non-interactive --agree-tos --redirect \
-m "admin@${DOMAIN}" 2>/dev/null; then
echo " SSL OK"
else
echo " SSL noch nicht moeglich (DNS/Cloudflare noch nicht aktiv) — HTTP auf Port 80 ist vorbereitet"
fi
fi
# Cloudflare Tunnel
if [[ -n "${CLOUDFLARE_TUNNEL_TOKEN:-}" ]]; then
echo "==> Cloudflare Tunnel"
cloudflared service uninstall 2>/dev/null || true
cloudflared service install "${CLOUDFLARE_TUNNEL_TOKEN}"
systemctl enable cloudflared
systemctl restart cloudflared
echo " Im Cloudflare-Dashboard Public Hostname setzen:"
echo " ${DOMAIN} + www.${DOMAIN} → http://127.0.0.1:${PORT}"
fi
nginx -t && systemctl reload nginx
echo ""
echo "=== Fertig ==="
echo "App: http://127.0.0.1:${PORT}/"
echo "LAN: http://192.168.178.49/"
echo "Domain: http://${DOMAIN}/ (sobald DNS auf Homelab/Cloudflare zeigt)"
echo "HTTPS: https://${DOMAIN}/ (nach Cloudflare Active + Tunnel ODER Certbot)"

15
deploy/duckdns-update.bat Normal file
View File

@@ -0,0 +1,15 @@
@echo off
REM DuckDNS IP-Updater fuer Windows (optional, falls kein Linux-Homelab-Timer)
setlocal
cd /d "%~dp0.."
if not exist "deploy\env.homelab" (
echo deploy\env.homelab fehlt
exit /b 1
)
for /f "usebackq tokens=1,2 delims==" %%a in ("deploy\env.homelab") do (
if "%%a"=="DUCKDNS_TOKEN" set DUCKDNS_TOKEN=%%b
if "%%a"=="DUCKDNS_DOMAIN" set DUCKDNS_DOMAIN=%%b
)
for /f "delims=" %%i in ('powershell -NoProfile -Command "(Invoke-WebRequest -Uri 'https://api.ipify.org' -UseBasicParsing).Content"') do set IPV4=%%i
for /f "tokens=1 delims=." %%s in ("%DUCKDNS_DOMAIN%") do set SUB=%%s
powershell -NoProfile -Command "$r=(Invoke-WebRequest -Uri 'https://www.duckdns.org/update?domains=%SUB%&token=%DUCKDNS_TOKEN%&verbose=true&ip=%IPV4%' -UseBasicParsing).Content; Write-Host (Get-Date -Format o) 'duckdns' '%SUB%:' $r 'ip=' '%IPV4%'"

32
deploy/duckdns-update.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# DuckDNS IP-Updater — holt oeffentliche IPv4/IPv6 und meldet sie an DuckDNS.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/env.homelab"
: "${DUCKDNS_TOKEN:?DUCKDNS_TOKEN fehlt in deploy/env.homelab}"
: "${DUCKDNS_DOMAIN:?DUCKDNS_DOMAIN fehlt in deploy/env.homelab}"
SUBDOMAIN="${DUCKDNS_DOMAIN%%.*}"
IPV4="$(curl -4 -fsS --max-time 15 https://api.ipify.org 2>/dev/null || true)"
IPV6="$(curl -6 -fsS --max-time 15 https://api64.ipify.org 2>/dev/null || true)"
if [[ -z "${IPV4}" && -z "${IPV6}" ]]; then
echo "duckdns-update: keine oeffentliche IP ermittelbar" >&2
exit 1
fi
URL="https://www.duckdns.org/update?domains=${SUBDOMAIN}&token=${DUCKDNS_TOKEN}&verbose=true"
[[ -n "${IPV4}" ]] && URL+="&ip=${IPV4}"
[[ -n "${IPV6}" ]] && URL+="&ipv6=${IPV6}"
RESP="$(curl -fsS --max-time 20 "${URL}")"
echo "$(date -Is) duckdns ${SUBDOMAIN}: ${RESP} (v4=${IPV4:-none} v6=${IPV6:-none})"
case "${RESP}" in
OK*) exit 0 ;;
*) exit 1 ;;
esac

View File

@@ -0,0 +1,33 @@
# Homelab-Konfiguration (deploy/env.homelab auf dem Server — NICHT committen)
HOMELAB_SSH=gizzler@192.168.178.49
# === Hauptdomain (Strato, DNS bei Cloudflare) ===
APP_DOMAIN=trilogyhub.de
# Cloudflare Tunnel (Zero Trust → Networks → Tunnels → dein Tunnel → Install command / Token)
# Public Hostnames im Dashboard eintragen:
# trilogyhub.de → http://127.0.0.1:3080
# www.trilogyhub.de → http://127.0.0.1:3080
CLOUDFLARE_TUNNEL_TOKEN=
# Cloudflare Dynamic DNS — meldet Fritz!Box-WAN-IP an Cloudflare (alle 5 Min)
# API Token: My Profile → API Tokens → Create → Zone DNS Edit (nur trilogyhub.de)
# Zone ID: Cloudflare Dashboard → trilogyhub.de → Overview → rechts „Zone ID“
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ZONE_ID=
# Welcher DNS-Name bekommt die aktuelle IP? (home.* empfohlen wenn Hauptdomain per Tunnel laeuft)
DDNS_RECORD_NAME=home.trilogyhub.de
DDNS_PROXIED=true
# Legacy DuckDNS (optional, kann leer bleiben)
DUCKDNS_TOKEN=
DUCKDNS_DOMAIN=
# PlayBull
APP_PORT=3080
APP_DIR=/home/gizzler/bestewebsite/bestewebsite
APP_USER=gizzler
# App-Secrets
JWT_SECRET=bitte-aendern
FINNHUB_API_KEY=

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Auf dem Homelab ausfuehren (nach push-homelab.ps1), wenn der Installer per SSH abbrach:
# cd ~/bestewebsite/bestewebsite && bash deploy/finish-on-server.sh
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$DIR"
chmod +x deploy/install-homelab.sh deploy/duckdns-update.sh 2>/dev/null || true
echo "==> PlayBull Installer (braucht sudo-Passwort auf dem Server)"
echo " Ziel: ${APP_DIR} (playbull.service)"
sudo bash deploy/install-homelab.sh
echo ""
echo "Pruefen:"
systemctl status playbull --no-pager || true

135
deploy/fix-domain.sh Normal file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env bash
# Domain-Migration auf dem Homelab (DuckDNS + Nginx + Certbot)
# Usage: sudo bash deploy/fix-domain.sh [neue-domain]
set -euo pipefail
NEW_DOMAIN="${1:-trilogyhub.de}"
OLD_DOMAIN="${2:-halmc.duckdns.org}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
if [[ "${EUID}" -ne 0 ]]; then
echo "Bitte als root: sudo bash deploy/fix-domain.sh" >&2
exit 1
fi
ENV_FILE="${APP_DIR}/deploy/env.homelab"
if [[ ! -f "${ENV_FILE}" ]]; then
echo "deploy/env.homelab fehlt" >&2
exit 1
fi
echo "==> Domain: ${OLD_DOMAIN} -> ${NEW_DOMAIN}"
# env.homelab aktualisieren (Token behalten, CRLF entfernen)
sed -i 's/\r$//' "${ENV_FILE}"
if grep -q '^APP_DOMAIN=' "${ENV_FILE}"; then
sed -i "s|^APP_DOMAIN=.*|APP_DOMAIN=${NEW_DOMAIN}|" "${ENV_FILE}"
else
echo "APP_DOMAIN=${NEW_DOMAIN}" >> "${ENV_FILE}"
fi
if grep -q '^DUCKDNS_DOMAIN=' "${ENV_FILE}"; then
sed -i "s|^DUCKDNS_DOMAIN=.*|DUCKDNS_DOMAIN=${NEW_DOMAIN}|" "${ENV_FILE}"
else
echo "DUCKDNS_DOMAIN=${NEW_DOMAIN}" >> "${ENV_FILE}"
fi
chmod 600 "${ENV_FILE}"
# DuckDNS IP aktualisieren
if grep -q '^DUCKDNS_TOKEN=.\+' "${ENV_FILE}" 2>/dev/null; then
echo "==> DuckDNS Update"
bash "${APP_DIR}/deploy/duckdns-update.sh" || echo " DuckDNS-Update fehlgeschlagen (manuell pruefen)"
else
echo " DuckDNS-Token leer — Update uebersprungen"
fi
# Nginx LAN
echo "==> Nginx LAN"
sed -i 's/\r$//' "${APP_DIR}/deploy/nginx/bestewebsite-lan.conf" "${APP_DIR}/deploy/nginx/bestewebsite.conf" 2>/dev/null || true
cp "${APP_DIR}/deploy/nginx/bestewebsite-lan.conf" /etc/nginx/sites-available/bestewebsite-lan
ln -sf /etc/nginx/sites-available/bestewebsite-lan /etc/nginx/sites-enabled/00-bestewebsite-lan
# Nginx öffentlich — zuerst HTTP-only für Certbot
echo "==> Nginx HTTP (Certbot-Vorbereitung)"
cat > /etc/nginx/sites-available/bestewebsite <<EOF
server {
listen 80;
listen [::]:80;
server_name ${NEW_DOMAIN};
client_max_body_size 30m;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
EOF
ln -sf /etc/nginx/sites-available/bestewebsite /etc/nginx/sites-enabled/bestewebsite
nginx -t
systemctl reload nginx
# Let's Encrypt Zertifikat
echo "==> Certbot fuer ${NEW_DOMAIN}"
CERTBOT_EMAIL="admin@${NEW_DOMAIN}"
if certbot certificates 2>/dev/null | grep -q "${NEW_DOMAIN}"; then
certbot renew --nginx --quiet || true
else
certbot --nginx -d "${NEW_DOMAIN}" --non-interactive --agree-tos --redirect -m "${CERTBOT_EMAIL}" || {
echo "Certbot fehlgeschlagen — pruefe ob ${NEW_DOMAIN} auf diese Maschine zeigt (Port 80)." >&2
exit 1
}
fi
# Finales Nginx-Template — Certbot-Zertifikat einbinden (kein erneutes certbot install)
echo "==> Nginx HTTPS finalisieren"
cp "${APP_DIR}/deploy/nginx/bestewebsite.conf" /etc/nginx/sites-available/bestewebsite
if [[ ! -f "/etc/letsencrypt/live/${NEW_DOMAIN}/fullchain.pem" ]]; then
echo "Zertifikat fehlt fuer ${NEW_DOMAIN}" >&2
exit 1
fi
ln -sf /etc/nginx/sites-available/bestewebsite /etc/nginx/sites-enabled/bestewebsite
nginx -t
systemctl reload nginx
# Alte Domain optional auf neue weiterleiten (falls noch DNS zeigt)
if [[ "${OLD_DOMAIN}" != "${NEW_DOMAIN}" ]]; then
cat > /etc/nginx/sites-available/bestewebsite-legacy <<EOF
server {
listen 80;
listen [::]:80;
server_name ${OLD_DOMAIN};
return 301 https://${NEW_DOMAIN}\$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name ${OLD_DOMAIN};
ssl_certificate /etc/letsencrypt/live/${OLD_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${OLD_DOMAIN}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
return 301 https://${NEW_DOMAIN}\$request_uri;
}
EOF
if [[ -f "/etc/letsencrypt/live/${OLD_DOMAIN}/fullchain.pem" ]]; then
ln -sf /etc/nginx/sites-available/bestewebsite-legacy /etc/nginx/sites-enabled/bestewebsite-legacy
nginx -t && systemctl reload nginx
echo " Redirect ${OLD_DOMAIN} -> ${NEW_DOMAIN} aktiv"
else
rm -f /etc/nginx/sites-enabled/bestewebsite-legacy
fi
fi
systemctl restart playbull || true
echo ""
echo "=== Fertig ==="
echo "HTTPS: https://${NEW_DOMAIN}"
echo "LAN: http://192.168.178.49"
echo "Test: curl -sfI https://${NEW_DOMAIN}/ | head -3"

33
deploy/homelab-exec.ps1 Normal file
View File

@@ -0,0 +1,33 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Fuehrt einen Befehl auf dem Homelab aus (ohne interaktives SSH-Passwort).
.USAGE
.\deploy\homelab-exec.ps1 "systemctl status playbull --no-pager"
.\deploy\homelab-exec.ps1 -Sudo "bash deploy/install-homelab.sh"
#>
param(
[Parameter(Position = 0, Mandatory = $true)]
[string]$Command,
[switch]$Sudo,
[string]$WorkDir = "",
[int]$TimeoutSeconds = 120
)
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/homelab-lib.ps1"
$config = Get-HomelabConfig
$result = Invoke-HomelabRemote -Config $config -Command $Command -Sudo:$Sudo -WorkDir $WorkDir -TimeoutSeconds $TimeoutSeconds
if ($result.Output) {
$result.Output | ForEach-Object { Write-Output $_ }
}
if ($result.Error) {
$result.Error | ForEach-Object { [Console]::Error.WriteLine($_) }
}
if ($result.ExitStatus -ne 0) {
exit $result.ExitStatus
}

View File

@@ -0,0 +1,13 @@
#Requires -Version 5.1
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/homelab-lib.ps1"
$config = Get-HomelabConfig
$cmds = @(
"git status -sb",
"git diff --stat | head -30",
"curl -s http://127.0.0.1:3080/api/images | head -c 200"
)
foreach ($c in $cmds) {
Write-Host "`n>> $c" -ForegroundColor Cyan
& "$PSScriptRoot/homelab-exec.ps1" $c
}

130
deploy/homelab-lib.ps1 Normal file
View File

@@ -0,0 +1,130 @@
# Gemeinsame Homelab-Hilfen (credentials + Posh-SSH)
function Read-HomelabKeyValueFile {
param([string]$Path)
$map = @{}
if (-not (Test-Path $Path)) { return $map }
Get-Content $Path | ForEach-Object {
$line = $_.Trim()
if ($line -eq "" -or $line.StartsWith("#")) { return }
if ($line -match '^([^=]+)=(.*)$') {
$map[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
return $map
}
function Ensure-PoshSshModule {
if (-not (Get-Module -ListAvailable -Name Posh-SSH)) {
Install-Module -Name Posh-SSH -Scope CurrentUser -Force -AllowClobber
}
Import-Module Posh-SSH -ErrorAction Stop
}
function Get-HomelabConfig {
$deployDir = $PSScriptRoot
$credFile = Join-Path $deployDir "homelab.credentials"
if (-not (Test-Path $credFile)) {
throw "deploy/homelab.credentials fehlt."
}
$cred = Read-HomelabKeyValueFile $credFile
$envFile = Read-HomelabKeyValueFile (Join-Path $deployDir "env.homelab")
$hostName = $cred["HOMELAB_HOST"]
$user = $cred["HOMELAB_USER"]
$password = $cred["HOMELAB_PASSWORD"]
$sudoPassword = if ($cred["HOMELAB_SUDO_PASSWORD"]) { $cred["HOMELAB_SUDO_PASSWORD"] } else { $password }
if ([string]::IsNullOrWhiteSpace($hostName)) { $hostName = "192.168.178.49" }
if ([string]::IsNullOrWhiteSpace($user)) { $user = "gizzler" }
if ([string]::IsNullOrWhiteSpace($password)) {
throw "HOMELAB_PASSWORD in deploy/homelab.credentials ist leer."
}
$appDir = if ($cred["HOMELAB_APP_DIR"]) { $cred["HOMELAB_APP_DIR"] }
elseif ($envFile["APP_DIR"]) { $envFile["APP_DIR"] }
else { "/home/gizzler/bestewebsite/bestewebsite" }
$sec = ConvertTo-SecureString $password -AsPlainText -Force
$psCred = New-Object System.Management.Automation.PSCredential ($user, $sec)
return [PSCustomObject]@{
HostName = $hostName
User = $user
Password = $password
SudoPassword = $sudoPassword
AppDir = $appDir
Credential = $psCred
SshTarget = "${user}@${hostName}"
}
}
function Resolve-HomelabIPv4 {
param([string]$HostName)
if ($HostName -match '^\d+\.\d+\.\d+\.\d+$') { return $HostName }
try {
$v4 = [System.Net.Dns]::GetHostAddresses($HostName) |
Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork } |
Select-Object -First 1
if ($v4) { return $v4.ToString() }
} catch { }
return $HostName
}
function New-HomelabSession {
param($Config)
Ensure-PoshSshModule
$target = Resolve-HomelabIPv4 $Config.HostName
$session = New-SSHSession -ComputerName $target -Credential $Config.Credential -AcceptKey -ConnectionTimeout 30
if (-not $session) { throw "SSH-Verbindung zu $($Config.User)@${target} fehlgeschlagen." }
return $session
}
function Invoke-HomelabRemote {
param(
$Config,
[string]$Command,
[switch]$Sudo,
[string]$WorkDir = "",
[int]$TimeoutSeconds = 120
)
if ([string]::IsNullOrWhiteSpace($WorkDir)) { $WorkDir = $Config.AppDir }
$remoteCmd = $Command
if ($Sudo) {
$inner = if ($WorkDir) { "cd '$WorkDir' && $Command" } else { $Command }
$escaped = $inner.Replace("'", "'\''")
$remoteCmd = "echo '$($Config.SudoPassword)' | sudo -S bash -lc '$escaped'"
} elseif ($WorkDir) {
$remoteCmd = "cd '$WorkDir' && $Command"
}
$session = New-HomelabSession $Config
try {
$result = Invoke-SSHCommand -SessionId $session.SessionId -Command $remoteCmd -TimeOut $TimeoutSeconds
return $result
}
finally {
Remove-SSHSession -SessionId $session.SessionId | Out-Null
}
}
function Send-HomelabPath {
param(
$Config,
[string]$LocalPath,
[string]$RemoteDir
)
Ensure-PoshSshModule
$target = Resolve-HomelabIPv4 $Config.HostName
$remoteDir = ($RemoteDir.TrimEnd('/') + '/')
$sftp = New-SFTPSession -ComputerName $target -Credential $Config.Credential -AcceptKey
if (-not $sftp) { throw "SFTP-Verbindung zu ${target} fehlgeschlagen." }
try {
Set-SFTPItem -SessionId $sftp.SessionId -Path $LocalPath -Destination $remoteDir -Force
}
finally {
Remove-SFTPSession -SessionId $sftp.SessionId | Out-Null
}
}

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Auf dem Homelab: Trilogy pullen, PlayBull-Overlay sichern.
set -euo pipefail
cd "$(dirname "$0")/.."
git stash push -u -m "playbull-vor-trilogy-pull-$(date +%Y%m%d-%H%M)" -- \
. ':(exclude)node_modules' ':(exclude)node_modules/**' \
':(exclude)data' ':(exclude)data/**' \
':(exclude)playbull-deploy.tgz' ':(exclude)deploy/env.homelab' \
':(exclude)deploy/homelab.credentials' ':(exclude).env' || true
git pull origin main
git status -sb

View File

@@ -0,0 +1,13 @@
# Lokale Homelab-Zugangsdaten (NICHT committen)
# Kopieren nach: deploy/homelab.credentials
# Ziel auf dem Server: ~/bestewebsite/bestewebsite
HOMELAB_HOST=mikeshomelab
HOMELAB_USER=gizzler
HOMELAB_PASSWORD=dein-ssh-passwort
# Optional — falls sudo ein anderes Passwort braucht (sonst = HOMELAB_PASSWORD)
# HOMELAB_SUDO_PASSWORD=
# Optional — Arbeitsverzeichnis auf dem Server (Standard aus env.homelab)
# HOMELAB_APP_DIR=/home/gizzler/bestewebsite/bestewebsite

212
deploy/install-homelab.sh Normal file
View File

@@ -0,0 +1,212 @@
#!/usr/bin/env bash
# PlayBull Homelab-Installer — Ziel: ~/bestewebsite/bestewebsite
# DuckDNS + playbull.service + Cloudflare Tunnel (cloudflared service install)
set -euo pipefail
if [[ "${EUID}" -ne 0 ]]; then
echo "Bitte als root ausfuehren: sudo bash deploy/install-homelab.sh" >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
if [[ ! -f "${SCRIPT_DIR}/env.homelab" ]]; then
cp "${SCRIPT_DIR}/env.homelab.example" "${SCRIPT_DIR}/env.homelab"
echo "deploy/env.homelab erstellt — Token eintragen und erneut starten." >&2
exit 1
fi
# shellcheck source=/dev/null
source <(sed 's/\r$//' "${SCRIPT_DIR}/env.homelab")
APP_PORT="${APP_PORT:-3080}"
APP_DIR="${APP_DIR:-/home/gizzler/bestewebsite/bestewebsite}"
APP_DIR="${APP_DIR//$'\r'/}"
APP_USER="${APP_USER:-gizzler}"
APP_USER="${APP_USER//$'\r'/}"
DUCKDNS_DOMAIN="${DUCKDNS_DOMAIN//$'\r'/}"
DUCKDNS_TOKEN="${DUCKDNS_TOKEN//$'\r'/}"
APP_DOMAIN="${APP_DOMAIN:-trilogyhub.de}"
APP_DOMAIN="${APP_DOMAIN//$'\r'/}"
CLOUDFLARE_API_TOKEN="${CLOUDFLARE_API_TOKEN//$'\r'/}"
CLOUDFLARE_ZONE_ID="${CLOUDFLARE_ZONE_ID//$'\r'/}"
CLOUDFLARE_TUNNEL_TOKEN="${CLOUDFLARE_TUNNEL_TOKEN//$'\r'/}"
JWT_SECRET="${JWT_SECRET//$'\r'/}"
FINNHUB_API_KEY="${FINNHUB_API_KEY//$'\r'/}"
echo "==> PlayBull / Trilogy Hub Homelab Setup"
echo " Domain: ${APP_DOMAIN}"
echo " Port: ${APP_PORT}"
echo " Pfad: ${APP_DIR}"
echo ""
install_unit() {
local src="$1" dest="$2"
sed -e "s|__APP_DIR__|${APP_DIR}|g" \
-e "s|__APP_USER__|${APP_USER}|g" \
"${src}" > "${dest}"
}
need_pkg() { command -v "$1" >/dev/null 2>&1; }
if [[ "${SKIP_APT:-0}" != "1" ]] && command -v apt-get >/dev/null 2>&1; then
echo "==> Paketlisten aktualisieren..."
apt-get update -qq
PKGS=(curl ca-certificates rsync build-essential libvips-dev)
MISSING=()
for p in "${PKGS[@]}"; do need_pkg "$p" || MISSING+=("$p"); done
if ((${#MISSING[@]})); then
apt-get install -y "${MISSING[@]}"
fi
elif [[ "${SKIP_APT:-0}" != "1" ]] && command -v dnf >/dev/null 2>&1; then
dnf install -y curl ca-certificates rsync gcc-c++ make vips-devel
fi
if ! command -v cloudflared >/dev/null 2>&1; then
echo "==> cloudflared installieren"
ARCH="$(uname -m)"
case "${ARCH}" in
x86_64) CF_ARCH=amd64 ;;
aarch64|arm64) CF_ARCH=arm64 ;;
*) echo "Unsupported arch: ${ARCH}" >&2; exit 1 ;;
esac
curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${CF_ARCH}" \
-o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared
ln -sf /usr/local/bin/cloudflared /usr/bin/cloudflared
fi
if ! command -v node >/dev/null 2>&1 || [[ "$(node -p "process.versions.node.split('.')[0]")" -lt 24 ]]; then
echo "==> Node.js 24 installieren (NodeSource)"
if command -v apt-get >/dev/null 2>&1; then
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y nodejs
else
dnf install -y nodejs
fi
fi
mkdir -p "${APP_DIR}" "${APP_DIR}/data" "${APP_DIR}/public/uploads"
if [[ "${REPO_ROOT}" != "${APP_DIR}" ]]; then
echo "==> Dateien nach ${APP_DIR} synchronisieren"
rsync -a --delete \
--exclude node_modules \
--exclude data \
--exclude .git \
--exclude server.js \
--exclude server.py \
--exclude start.bat \
"${REPO_ROOT}/" "${APP_DIR}/"
fi
chown -R "${APP_USER}:${APP_USER}" "${APP_DIR}" 2>/dev/null || true
chmod +x "${APP_DIR}/deploy/duckdns-update.sh" \
"${APP_DIR}/deploy/cloudflare-ddns-update.sh" \
"${APP_DIR}/deploy/finish-on-server.sh" \
"${APP_DIR}/deploy/sync-user.sh" 2>/dev/null || true
# ---- .env (nur ergaenzen, nicht ueberschreiben) ----
ENV_FILE="${APP_DIR}/.env"
touch "${ENV_FILE}"
grep -q '^PORT=' "${ENV_FILE}" 2>/dev/null \
&& sed -i "s/^PORT=.*/PORT=${APP_PORT}/" "${ENV_FILE}" \
|| echo "PORT=${APP_PORT}" >> "${ENV_FILE}"
grep -q '^TRUST_PROXY=' "${ENV_FILE}" 2>/dev/null || echo "TRUST_PROXY=1" >> "${ENV_FILE}"
grep -q '^START_BALANCE=' "${ENV_FILE}" 2>/dev/null || echo "START_BALANCE=10000" >> "${ENV_FILE}"
grep -q '^JWT_SECRET=' "${ENV_FILE}" 2>/dev/null || { [[ -n "${JWT_SECRET:-}" ]] && echo "JWT_SECRET=${JWT_SECRET}" >> "${ENV_FILE}"; }
grep -q '^FINNHUB_API_KEY=' "${ENV_FILE}" 2>/dev/null || { [[ -n "${FINNHUB_API_KEY:-}" ]] && echo "FINNHUB_API_KEY=${FINNHUB_API_KEY}" >> "${ENV_FILE}"; }
if [[ "${APP_USER}" == "root" ]]; then
bash -c "cd '${APP_DIR}' && npm install --omit=dev"
else
sudo -u "${APP_USER}" bash -c "cd '${APP_DIR}' && npm install --omit=dev"
fi
# ---- Alten bestewebsite-Service abloesen ----
if systemctl is-active --quiet bestewebsite.service 2>/dev/null; then
echo "==> bestewebsite.service stoppen (Migration zu playbull)"
systemctl stop bestewebsite.service
fi
systemctl disable bestewebsite.service 2>/dev/null || true
# ---- Cloudflare Tunnel (wie bestewebsite: cloudflared service install) ----
if [[ -n "${CLOUDFLARE_TUNNEL_TOKEN:-}" ]]; then
echo "==> Cloudflare Tunnel"
systemctl stop cloudflared-playbull.service 2>/dev/null || true
systemctl disable cloudflared-playbull.service 2>/dev/null || true
cloudflared service uninstall 2>/dev/null || true
cloudflared service install "${CLOUDFLARE_TUNNEL_TOKEN}"
systemctl enable cloudflared
systemctl restart cloudflared
echo " cloudflared gestartet."
else
echo ""
echo ">>> CLOUDFLARE_TUNNEL_TOKEN fehlt in deploy/env.homelab"
echo " Zero Trust → Tunnel → Public Hostname: ${APP_DOMAIN} -> http://127.0.0.1:${APP_PORT}"
echo ""
fi
# ---- Cloudflare Dynamic DNS (ersetzt DuckDNS) ----
echo "==> Cloudflare DDNS Timer"
install_unit "${APP_DIR}/deploy/systemd/cloudflare-ddns-update.service" /etc/systemd/system/cloudflare-ddns-update.service
cp "${APP_DIR}/deploy/systemd/cloudflare-ddns-update.timer" /etc/systemd/system/
chmod 600 "${APP_DIR}/deploy/env.homelab"
if [[ -n "${CLOUDFLARE_API_TOKEN:-}" && -n "${CLOUDFLARE_ZONE_ID:-}" ]]; then
"${APP_DIR}/deploy/cloudflare-ddns-update.sh" || echo " Cloudflare-DDNS fehlgeschlagen (API/Zone pruefen)"
else
echo " Cloudflare-DDNS uebersprungen (CLOUDFLARE_API_TOKEN oder ZONE_ID leer)"
fi
# ---- DuckDNS (legacy, optional) ----
if [[ -n "${DUCKDNS_TOKEN:-}" ]]; then
echo "==> DuckDNS (legacy)"
install_unit "${APP_DIR}/deploy/systemd/duckdns-update.service" /etc/systemd/system/duckdns-update.service
cp "${APP_DIR}/deploy/systemd/duckdns-update.timer" /etc/systemd/system/
"${APP_DIR}/deploy/duckdns-update.sh" || true
fi
# ---- playbull systemd ----
echo "==> playbull.service"
install_unit "${APP_DIR}/deploy/systemd/playbull.service" /etc/systemd/system/playbull.service
systemctl daemon-reload
if [[ -n "${CLOUDFLARE_API_TOKEN:-}" && -n "${CLOUDFLARE_ZONE_ID:-}" ]]; then
systemctl enable --now cloudflare-ddns-update.timer
systemctl start cloudflare-ddns-update.service || true
else
systemctl disable cloudflare-ddns-update.timer 2>/dev/null || true
fi
if [[ -n "${DUCKDNS_TOKEN:-}" ]]; then
systemctl enable --now duckdns-update.timer
systemctl start duckdns-update.service || true
else
systemctl disable duckdns-update.timer 2>/dev/null || true
fi
systemctl enable --now playbull.service
systemctl restart playbull.service
# ---- Optional Nginx ----
if [[ "${USE_NGINX_VHOST:-0}" == "1" ]]; then
echo "==> Nginx VHost (optional)"
if command -v apt-get >/dev/null 2>&1; then
apt-get install -y nginx certbot python3-certbot-nginx
fi
cp "${APP_DIR}/deploy/nginx/playbull.conf" /etc/nginx/sites-available/playbull
ln -sf /etc/nginx/sites-available/playbull /etc/nginx/sites-enabled/playbull
certbot --nginx -d "${APP_DOMAIN}" -d "www.${APP_DOMAIN}" --non-interactive --agree-tos -m "admin@${APP_DOMAIN}" || true
nginx -t && systemctl reload nginx
fi
echo ""
echo "=== Fertig ==="
echo "Pfad: ${APP_DIR}"
echo "Intern: http://127.0.0.1:${APP_PORT}"
echo "DuckDNS: ${DUCKDNS_DOMAIN:-(deaktiviert)}"
echo "Status: systemctl status playbull cloudflared cloudflare-ddns-update.timer"
if [[ -n "${CLOUDFLARE_TUNNEL_TOKEN:-}" ]]; then
echo "HTTPS: https://${APP_DOMAIN}"
fi
echo ""
echo "Hinweis: JWT_SECRET und FINNHUB_API_KEY in ${ENV_FILE} pruefen, dann:"
echo " sudo systemctl restart playbull"

View File

@@ -0,0 +1,21 @@
# LAN / Fritz-Netz — HTTP auf Server-IP und Hostname (ohne TLS)
# Install: sudo cp deploy/nginx/bestewebsite-lan.conf /etc/nginx/sites-available/bestewebsite-lan
# sudo ln -sf /etc/nginx/sites-available/bestewebsite-lan /etc/nginx/sites-enabled/00-bestewebsite-lan
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name 192.168.178.49 mikeshomelab _;
client_max_body_size 30m;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
}

View File

@@ -0,0 +1,35 @@
# Öffentlicher HTTPS-VHost — DuckDNS → PlayBull :3080
# Install: sudo cp deploy/nginx/bestewebsite.conf /etc/nginx/sites-available/bestewebsite
# sudo ln -sf /etc/nginx/sites-available/bestewebsite /etc/nginx/sites-enabled/
# sudo certbot --nginx -d halmc.duckdns.org --non-interactive --agree-tos --redirect
# sudo nginx -t && sudo systemctl reload nginx
server {
server_name halmc.duckdns.org;
client_max_body_size 30m;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
listen [::]:443 ssl;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/halmc.duckdns.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/halmc.duckdns.org/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
listen 80;
listen [::]:80;
server_name halmc.duckdns.org;
return 301 https://$host$request_uri;
}

View File

@@ -0,0 +1,43 @@
# Optional: 3. Website auf bestehendem Nginx :443 (SNI).
# PlayBull laeuft intern auf Port 3080 — kein Konflikt mit anderen Sites.
#
# Install: sudo cp deploy/nginx/playbull.conf /etc/nginx/sites-available/playbull
# sudo ln -sf /etc/nginx/sites-available/playbull /etc/nginx/sites-enabled/
# sudo certbot --nginx -d halmc.duckdns.org
# sudo nginx -t && sudo systemctl reload nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name halmc.duckdns.org;
# certbot legt Zertifikate an:
# ssl_certificate /etc/letsencrypt/live/halmc.duckdns.org/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/halmc.duckdns.org/privkey.pem;
client_max_body_size 10m;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400;
}
}
server {
listen 80;
listen [::]:80;
server_name halmc.duckdns.org;
return 301 https://$host$request_uri;
}

View File

@@ -0,0 +1,33 @@
# Öffentlicher HTTPS-VHost — trilogyhub.de (+ optional www)
# Mit Cloudflare Tunnel: oft nicht noetig (Tunnel terminiert HTTPS).
# Mit Nginx + Let's Encrypt: certbot --nginx -d trilogyhub.de -d www.trilogyhub.de
server {
server_name trilogyhub.de www.trilogyhub.de;
client_max_body_size 30m;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
listen [::]:443 ssl;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/trilogyhub.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/trilogyhub.de/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
listen 80;
listen [::]:80;
server_name trilogyhub.de www.trilogyhub.de;
return 301 https://$host$request_uri;
}

139
deploy/push-homelab.ps1 Normal file
View File

@@ -0,0 +1,139 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Kopiert PlayBull nach ~/bestewebsite/bestewebsite und startet install-homelab.sh.
Nutzt deploy/homelab.credentials (Posh-SSH) wenn vorhanden.
#>
param(
[string]$SshTarget = "",
[string]$RemoteDir = "",
[switch]$SkipInstall,
[switch]$UserOnly
)
$ErrorActionPreference = "Stop"
$Root = Split-Path $PSScriptRoot -Parent
$CredFile = Join-Path $PSScriptRoot "homelab.credentials"
$UseCredentials = Test-Path $CredFile
function Assert-LastExit {
param([string]$Step)
if ($LASTEXITCODE -ne 0) {
throw "${Step} fehlgeschlagen (Exit ${LASTEXITCODE})."
}
}
$EnvFile = Join-Path $PSScriptRoot "env.homelab"
if (-not (Test-Path $EnvFile)) {
Write-Error "deploy/env.homelab fehlt."
}
$envContent = Get-Content $EnvFile -Raw
if ($RemoteDir -eq "" -and $envContent -match '(?m)^APP_DIR=(.+)$') {
$RemoteDir = $Matches[1].Trim()
}
if ([string]::IsNullOrWhiteSpace($RemoteDir)) {
$RemoteDir = "/home/gizzler/bestewebsite/bestewebsite"
}
if ($UseCredentials) {
. "$PSScriptRoot/homelab-lib.ps1"
$config = Get-HomelabConfig
if ($SshTarget -eq "") { $SshTarget = $config.SshTarget }
Write-Host "==> Sync nach $($config.SshTarget):${RemoteDir} (homelab.credentials)" -ForegroundColor Cyan
Invoke-HomelabRemote -Config $config -Command "mkdir -p '$RemoteDir'" -WorkDir "" | Out-Null
$archiveWin = Join-Path $env:TEMP "playbull-deploy.tgz"
if (Test-Path $archiveWin) { Remove-Item $archiveWin -Force }
$tarExe = Join-Path $env:SystemRoot "System32\tar.exe"
$tarItems = @("server", "public", "package.json", "package-lock.json", "deploy", ".env.example")
Push-Location $Root
try {
& $tarExe -czf $archiveWin @tarItems
if ($LASTEXITCODE -ne 0) { throw "tar-Archiv fehlgeschlagen." }
}
finally { Pop-Location }
$sizeMb = [math]::Round((Get-Item $archiveWin).Length / 1MB, 1)
Write-Host " -> Archiv ${sizeMb} MB" -ForegroundColor DarkGray
Send-HomelabPath -Config $config -LocalPath $archiveWin -RemoteDir $RemoteDir
$extractCmd = @'
cp deploy/env.homelab deploy/env.homelab.bak 2>/dev/null || true
tar xzf playbull-deploy.tgz && rm -f playbull-deploy.tgz
if [ -f deploy/env.homelab.bak ]; then
for key in DUCKDNS_TOKEN CLOUDFLARE_TUNNEL_TOKEN CLOUDFLARE_API_TOKEN CLOUDFLARE_ZONE_ID JWT_SECRET FINNHUB_API_KEY APP_DOMAIN; do
old=$(grep -m1 "^${key}=" deploy/env.homelab.bak 2>/dev/null | cut -d= -f2- | tr -d '\r')
if [ -n "$old" ]; then
if grep -q "^${key}=" deploy/env.homelab 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${old}|" deploy/env.homelab
else
echo "${key}=${old}" >> deploy/env.homelab
fi
fi
done
rm -f deploy/env.homelab.bak
fi
sed -i 's/\r$//' deploy/env.homelab deploy/*.sh deploy/nginx/*.conf 2>/dev/null || true
'@
$extract = Invoke-HomelabRemote -Config $config -Command $extractCmd -WorkDir $RemoteDir -TimeoutSeconds 300
$extract.Output | ForEach-Object { Write-Output $_ }
if ($extract.ExitStatus -ne 0) { exit $extract.ExitStatus }
Remove-Item $archiveWin -Force -ErrorAction SilentlyContinue
if ($UserOnly) {
Write-Host "==> User-Sync"
$r = Invoke-HomelabRemote -Config $config -Command "bash deploy/sync-user.sh" -WorkDir $RemoteDir -TimeoutSeconds 300
$r.Output | ForEach-Object { Write-Output $_ }
if ($r.ExitStatus -ne 0) { exit $r.ExitStatus }
Write-Host "Fertig (User-Modus)." -ForegroundColor Green
exit 0
}
if ($SkipInstall) {
Write-Host "Sync fertig. Installer: .\deploy\homelab-exec.ps1 -Sudo 'bash deploy/install-homelab.sh'" -ForegroundColor Cyan
exit 0
}
Write-Host "==> Installer auf Homelab"
$install = Invoke-HomelabRemote -Config $config -Sudo -Command "chmod +x deploy/*.sh && bash deploy/install-homelab.sh" -WorkDir $RemoteDir -TimeoutSeconds 900
$install.Output | ForEach-Object { Write-Output $_ }
if ($install.Error) { $install.Error | ForEach-Object { [Console]::Error.WriteLine($_) } }
if ($install.ExitStatus -ne 0) { exit $install.ExitStatus }
Write-Host ""
Write-Host "==> Verifikation" -ForegroundColor Cyan
$verify = Invoke-HomelabRemote -Config $config -Command "systemctl is-active playbull cloudflared 2>/dev/null; curl -sf http://127.0.0.1:3080/api/market | head -c 80; echo; test -f public/image-editor.js && echo image-editor.js OK" -WorkDir $RemoteDir
$verify.Output | ForEach-Object { Write-Output $_ }
if ($verify.ExitStatus -ne 0) { exit $verify.ExitStatus }
Write-Host ""
Write-Host "Fertig. https://trilogyhub.de" -ForegroundColor Green
exit 0
}
# Fallback: interaktives ssh/scp
if ($SshTarget -eq "" -and $envContent -match '(?m)^HOMELAB_SSH=(.+)$') {
$SshTarget = $Matches[1].Trim()
}
if ([string]::IsNullOrWhiteSpace($SshTarget) -or $SshTarget -eq "root@dein-server") {
Write-Error "HOMELAB_SSH setzen oder deploy/homelab.credentials anlegen."
}
Write-Host "SSH mit Passwort (interaktiv)..." -ForegroundColor Yellow
ssh $SshTarget "mkdir -p '$RemoteDir'"
Assert-LastExit "ssh mkdir"
$items = @("server", "public", "package.json", "package-lock.json", "deploy", ".env.example")
foreach ($item in $items) {
$local = Join-Path $Root $item
if (Test-Path $local) {
scp -r $local "${SshTarget}:${RemoteDir}/"
Assert-LastExit "scp $item"
}
}
if ($SkipInstall) { exit 0 }
ssh -t $SshTarget "cd '$RemoteDir' && chmod +x deploy/*.sh && sudo bash deploy/install-homelab.sh"
Assert-LastExit "install-homelab.sh"

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Schnellstart auf dem Homelab als normaler User (gizzler)
# DuckDNS + systemd brauchen einmal: sudo bash deploy/install-homelab.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "${SCRIPT_DIR}/.."
echo "==> PlayBull Quick-Start ($(whoami)@$(hostname))"
if [[ ! -f .env ]]; then
echo "Erstelle .env — bitte Werte eintragen:"
cp -n .env.example .env 2>/dev/null || true
${EDITOR:-nano} .env
fi
if [[ ! -f deploy/env.homelab ]]; then
cp deploy/env.homelab.example deploy/env.homelab
${EDITOR:-nano} deploy/env.homelab
fi
command -v node >/dev/null || { echo "Node.js fehlt. Bitte installieren (v24+)."; exit 1; }
npm install --omit=dev
echo ""
echo "Test starten: npm start"
echo "Dauerbetrieb: sudo bash deploy/install-homelab.sh (fragt sudo-Passwort)"
echo ""

4
deploy/setup-all.bat Normal file
View File

@@ -0,0 +1,4 @@
@echo off
cd /d "%~dp0.."
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-all.ps1"
pause

11
deploy/setup-all.ps1 Normal file
View File

@@ -0,0 +1,11 @@
#Requires -Version 5.1
# Ein-Klick: Sync + Installer + Verifikation auf dem Homelab
$ErrorActionPreference = "Stop"
Set-Location (Split-Path $PSScriptRoot -Parent)
Write-Host "=== PlayBull Homelab Setup ===" -ForegroundColor Cyan
& "$PSScriptRoot\push-homelab.ps1"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host ""
Write-Host "=== Status ===" -ForegroundColor Cyan
& "$PSScriptRoot\homelab-exec.ps1" "systemctl status playbull cloudflared --no-pager -l | head -40"
& "$PSScriptRoot\homelab-exec.ps1" "grep -E '^(PORT|JWT_SECRET|FINNHUB_API_KEY)=' .env | sed 's/=.*/=***/'"

32
deploy/sync-user.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Ohne systemd/sudo: Dateien nach ~/playbull kopieren und npm install.
# Auf dem Homelab: cd ~/bestewebsite/bestewebsite && bash deploy/sync-user.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=/dev/null
[[ -f "${SCRIPT_DIR}/env.homelab" ]] && source "${SCRIPT_DIR}/env.homelab"
APP_DIR="${APP_DIR:-/home/gizzler/bestewebsite/bestewebsite}"
APP_PORT="${APP_PORT:-3080}"
mkdir -p "${APP_DIR}"
rsync -a --delete \
--exclude node_modules --exclude data --exclude .git \
"${SRC}/" "${APP_DIR}/"
cd "${APP_DIR}"
command -v node >/dev/null || { echo "Node.js fehlt (v24+)."; exit 1; }
npm install --omit=dev
ENV="${APP_DIR}/.env"
touch "${ENV}"
grep -q '^PORT=' "${ENV}" 2>/dev/null && sed -i "s/^PORT=.*/PORT=${APP_PORT}/" "${ENV}" || echo "PORT=${APP_PORT}" >> "${ENV}"
grep -q '^TRUST_PROXY=' "${ENV}" 2>/dev/null || echo "TRUST_PROXY=1" >> "${ENV}"
echo ""
echo "=== Sync fertig: ${APP_DIR} ==="
echo "Start (Test): cd ${APP_DIR} && node server/index.js"
echo "Dauerbetrieb: bash deploy/finish-on-server.sh (sudo + systemd)"
echo ""
echo "Falls schon laeuft: pkill -f 'node server/index.js' ; cd ${APP_DIR} && nohup node server/index.js >> data/server.log 2>&1 &"

View File

@@ -0,0 +1,12 @@
[Unit]
Description=Cloudflare Dynamic DNS updater (Trilogy Hub)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=__APP_DIR__/deploy/cloudflare-ddns-update.sh
User=root
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Cloudflare DDNS alle 5 Minuten pruefen
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,17 @@
[Unit]
Description=Cloudflare Tunnel for PlayBull (HTTPS)
After=network-online.target playbull.service
Wants=network-online.target
Requires=playbull.service
[Service]
Type=simple
User=root
EnvironmentFile=/opt/playbull/deploy/env.homelab
# Token-Modus: einfachster Weg aus dem Zero-Trust-Dashboard
ExecStart=/bin/bash -c 'exec /usr/bin/cloudflared tunnel --no-autoupdate run --token "$CLOUDFLARE_TUNNEL_TOKEN"'
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,12 @@
[Unit]
Description=DuckDNS IP updater for PlayBull homelab
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=__APP_DIR__/deploy/duckdns-update.sh
User=root
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Run DuckDNS IP update every 5 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
AccuracySec=1min
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,18 @@
[Unit]
Description=PlayBull — Trilogy Hub + Trading + Casino
After=network.target
[Service]
Type=simple
User=__APP_USER__
Group=__APP_USER__
WorkingDirectory=__APP_DIR__
EnvironmentFile=__APP_DIR__/.env
Environment=NODE_ENV=production
Environment=TRUST_PROXY=1
ExecStart=/usr/bin/node __APP_DIR__/server/index.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

5
deploy/test-ports.ps1 Normal file
View File

@@ -0,0 +1,5 @@
$ip = "192.168.178.49"
foreach ($port in 22, 80, 443, 3080) {
$r = Test-NetConnection -ComputerName $ip -Port $port -WarningAction SilentlyContinue
Write-Output "Port ${port}: $($r.TcpTestSucceeded)"
}