Deployment-Fixes (Dienst startete nicht -> 203/EXEC "uvicorn not found"): - Unit ohne ProtectHome/ProtectSystem: Repo/venv dürfen unter /root oder /home liegen (ProtectHome=true blendete diese Pfade für den Dienst aus). - install.sh: Python-3.11+-Prüfung mit klarer Meldung; prüft nach der Installation, dass uvicorn wirklich in der venv liegt. - EZB-Kursabruf läuft beim Start jetzt im Hintergrund (blockiert den Start nie). Phase-3-Backend (Dashboard): - Modell + Migration 0004: dashboard_layouts (JSON-Layout je Nutzer). - routers/dashboard: Layout speichern/laden/zurücksetzen + Kartendaten (Budget-Donut, Budget-Fortschritt, Einnahmen/Ausgaben, Kontostände, Vermögensverlauf, letzte Buchungen). - Frontend-Infrastruktur: Chart-Komponenten (Donut/Line), palette, API-Methoden, react-grid-layout. (Dashboard-Seite folgt im nächsten Commit.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
215 lines
7.9 KiB
Bash
Executable File
215 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# Certain Finance – nativer Installer für einen Debian/Ubuntu-LXC (Proxmox).
|
||
# Richtet Python-venv, gebautes Frontend, systemd-Dienst und Konfiguration ein.
|
||
# HTTPS macht dein vorhandener Nginx Proxy Manager – hier wird KEIN Reverse-Proxy
|
||
# installiert; der Dienst lauscht nur intern auf 127.0.0.1:<APP_PORT>.
|
||
#
|
||
# Nutzung (als root im geklonten Repo):
|
||
# ./deploy/install.sh interaktiv (fragt Port/Admin)
|
||
# ./deploy/install.sh --yes nicht-interaktiv (Zufalls-Admin-Passwort)
|
||
#
|
||
set -euo pipefail
|
||
|
||
APP_SLUG="certain-finance" # Einzige Stelle mit dem Betriebs-Slug (Dienst/Pfade)
|
||
DEFAULT_PORT=8071
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
ETC_DIR="/etc/$APP_SLUG"
|
||
ENV_FILE="$ETC_DIR/env"
|
||
DATA_DIR="/var/lib/$APP_SLUG"
|
||
SERVICE_USER="$APP_SLUG"
|
||
UNIT_FILE="/etc/systemd/system/$APP_SLUG.service"
|
||
|
||
ASSUME_YES=0
|
||
[[ "${1:-}" == "--yes" || "${1:-}" == "-y" ]] && ASSUME_YES=1
|
||
|
||
info() { printf '\033[0;32m==>\033[0m %s\n' "$*"; }
|
||
warn() { printf '\033[0;33m[!]\033[0m %s\n' "$*"; }
|
||
error() { printf '\033[0;31m[x]\033[0m %s\n' "$*" >&2; }
|
||
rand() { LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c "${1:-32}"; }
|
||
|
||
if [[ "$(id -u)" -ne 0 ]]; then
|
||
error "Bitte als root ausführen (z. B. 'sudo ./deploy/install.sh')."
|
||
exit 1
|
||
fi
|
||
|
||
if ! command -v apt-get >/dev/null 2>&1; then
|
||
error "Dieses Skript ist für Debian/Ubuntu (apt) gedacht. Kein apt-get gefunden."
|
||
exit 1
|
||
fi
|
||
|
||
# --- Systempakete ------------------------------------------------------------
|
||
info "Installiere Systempakete (Python, Build-Tools, git, curl) …"
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
apt-get update -y
|
||
apt-get install -y python3 python3-venv python3-dev build-essential curl ca-certificates git
|
||
|
||
# --- Python-Version prüfen (3.11+ erforderlich) ------------------------------
|
||
PY_VER="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null || echo 0.0)"
|
||
PY_MAJOR="${PY_VER%%.*}"
|
||
PY_MINOR="${PY_VER##*.}"
|
||
if [[ "$PY_MAJOR" -lt 3 || ( "$PY_MAJOR" -eq 3 && "$PY_MINOR" -lt 11 ) ]]; then
|
||
error "Python 3.11+ wird benötigt – gefunden: $PY_VER."
|
||
error "Bitte einen Debian-12- oder Ubuntu-24.04-LXC verwenden (dort ist Python 3.11+ dabei)."
|
||
error "Ältere Systeme (z. B. Debian 11 = Python 3.9) werden nicht unterstützt."
|
||
exit 1
|
||
fi
|
||
info "Python $PY_VER gefunden."
|
||
|
||
# --- Node.js (nur zum Bauen des Frontends nötig) -----------------------------
|
||
ensure_node() {
|
||
local major=0
|
||
if command -v node >/dev/null 2>&1; then
|
||
major="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
|
||
fi
|
||
if [[ "$major" -ge 18 ]]; then
|
||
info "Node.js gefunden: $(node --version)"
|
||
return
|
||
fi
|
||
warn "Node.js fehlt oder ist zu alt – installiere Node 20 LTS (NodeSource) …"
|
||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||
apt-get install -y nodejs
|
||
info "Node.js installiert: $(node --version)"
|
||
}
|
||
ensure_node
|
||
|
||
# --- Dienst-Benutzer und Verzeichnisse --------------------------------------
|
||
if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then
|
||
info "Lege Dienst-Benutzer '$SERVICE_USER' an …"
|
||
useradd --system --user-group --home-dir "$DATA_DIR" --shell /usr/sbin/nologin "$SERVICE_USER"
|
||
fi
|
||
mkdir -p "$DATA_DIR" "$ETC_DIR"
|
||
chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR"
|
||
|
||
# --- Python-venv + Abhängigkeiten -------------------------------------------
|
||
info "Richte Python-venv ein und installiere Abhängigkeiten …"
|
||
python3 -m venv "$REPO_DIR/backend/.venv"
|
||
"$REPO_DIR/backend/.venv/bin/pip" install --upgrade pip >/dev/null
|
||
"$REPO_DIR/backend/.venv/bin/pip" install -r "$REPO_DIR/backend/requirements.txt"
|
||
if [[ ! -x "$REPO_DIR/backend/.venv/bin/uvicorn" ]]; then
|
||
error "uvicorn wurde nicht in die venv installiert – die Abhängigkeiten sind unvollständig."
|
||
error "Bitte die Ausgabe oben auf pip-Fehler prüfen."
|
||
exit 1
|
||
fi
|
||
|
||
# --- Frontend bauen ----------------------------------------------------------
|
||
info "Baue das Frontend (Node wird nur hier gebraucht) …"
|
||
(
|
||
cd "$REPO_DIR/web"
|
||
if [[ -f package-lock.json ]]; then npm ci; else npm install; fi
|
||
npm run build
|
||
)
|
||
|
||
# --- Konfiguration (.env) ----------------------------------------------------
|
||
GIT_SHA="$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo '')"
|
||
if [[ -f "$ENV_FILE" ]]; then
|
||
info "Konfiguration existiert bereits: $ENV_FILE (unverändert übernommen)."
|
||
# Git-Commit aktualisieren (für /api/version).
|
||
if grep -q '^APP_GIT_SHA=' "$ENV_FILE"; then
|
||
sed -i "s|^APP_GIT_SHA=.*|APP_GIT_SHA=$GIT_SHA|" "$ENV_FILE"
|
||
else
|
||
echo "APP_GIT_SHA=$GIT_SHA" >> "$ENV_FILE"
|
||
fi
|
||
# Direktzugriff (http://IP:Port) für ältere Konfigurationen nachrüsten.
|
||
grep -q '^APP_BIND_HOST=' "$ENV_FILE" || echo "APP_BIND_HOST=0.0.0.0" >> "$ENV_FILE"
|
||
ADMIN_USER="$(grep -E '^APP_ADMIN_USERNAME=' "$ENV_FILE" | cut -d= -f2-)"
|
||
GENERATED_PASS=""
|
||
else
|
||
local_port="$DEFAULT_PORT"
|
||
admin_user="admin"
|
||
admin_pass="$(rand 16)"
|
||
if [[ "$ASSUME_YES" -eq 0 ]]; then
|
||
read -r -p "Interner Port [$DEFAULT_PORT]: " x; local_port="${x:-$DEFAULT_PORT}"
|
||
read -r -p "Admin-Benutzername [admin]: " x; admin_user="${x:-admin}"
|
||
read -r -s -p "Admin-Passwort [leer = zufällig]: " x; echo
|
||
[[ -n "$x" ]] && admin_pass="$x"
|
||
fi
|
||
info "Erzeuge Konfiguration: $ENV_FILE"
|
||
cat > "$ENV_FILE" <<EOF
|
||
APP_DATA_DIR=$DATA_DIR
|
||
APP_BIND_HOST=0.0.0.0
|
||
APP_PORT=$local_port
|
||
APP_SESSION_SECRET=$(rand 48)
|
||
APP_COOKIE_SECURE=false
|
||
APP_ADMIN_USERNAME=$admin_user
|
||
APP_ADMIN_PASSWORD=$admin_pass
|
||
APP_DEFAULT_BASE_CURRENCY=EUR
|
||
APP_ECB_FETCH_ON_START=true
|
||
APP_GIT_SHA=$GIT_SHA
|
||
EOF
|
||
ADMIN_USER="$admin_user"
|
||
GENERATED_PASS="$admin_pass"
|
||
fi
|
||
chown root:root "$ENV_FILE"
|
||
chmod 600 "$ENV_FILE"
|
||
|
||
# --- systemd-Unit ------------------------------------------------------------
|
||
info "Schreibe systemd-Unit: $UNIT_FILE"
|
||
cat > "$UNIT_FILE" <<EOF
|
||
[Unit]
|
||
Description=Certain Finance
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=$SERVICE_USER
|
||
Group=$SERVICE_USER
|
||
WorkingDirectory=$REPO_DIR/backend
|
||
EnvironmentFile=$ENV_FILE
|
||
ExecStart=$REPO_DIR/backend/.venv/bin/uvicorn app.main:app --host \${APP_BIND_HOST} --port \${APP_PORT}
|
||
Restart=on-failure
|
||
RestartSec=3
|
||
NoNewPrivileges=true
|
||
PrivateTmp=true
|
||
# Bewusst KEIN ProtectHome/ProtectSystem: das Repo (inkl. venv) darf auch unter
|
||
# /root oder /home liegen; ProtectHome=true würde diese Pfade für den Dienst
|
||
# ausblenden -> "Failed to locate executable .../uvicorn".
|
||
ReadWritePaths=$DATA_DIR
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable "$APP_SLUG" >/dev/null 2>&1 || true
|
||
info "Starte Dienst … (wendet Migrationen an und legt ggf. den ersten Admin an)"
|
||
systemctl restart "$APP_SLUG"
|
||
|
||
# --- Gesundheitscheck --------------------------------------------------------
|
||
PORT="$(grep -E '^APP_PORT=' "$ENV_FILE" | cut -d= -f2-)"
|
||
PORT="${PORT:-$DEFAULT_PORT}"
|
||
info "Warte auf das Backend (127.0.0.1:$PORT) …"
|
||
ok=0
|
||
for _ in $(seq 1 30); do
|
||
if curl -fsS "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then ok=1; break; fi
|
||
sleep 2
|
||
done
|
||
|
||
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||
|
||
echo
|
||
if [[ "$ok" -eq 1 ]]; then
|
||
info "Fertig – Certain Finance läuft. 🎉"
|
||
else
|
||
warn "Backend antwortet noch nicht. Logs: journalctl -u $APP_SLUG -e"
|
||
fi
|
||
echo " Web-UI (direkt): http://${LAN_IP:-<LXC-IP>}:$PORT"
|
||
echo " Admin-Login: ${ADMIN_USER:-admin}"
|
||
if [[ -n "${GENERATED_PASS:-}" ]]; then
|
||
echo " Admin-Passwort: ${GENERATED_PASS}"
|
||
warn "Bitte dieses Passwort notieren – es wird nicht erneut angezeigt."
|
||
fi
|
||
echo
|
||
echo " Optional über HTTPS (Nginx Proxy Manager): neuen Proxy-Host ->"
|
||
echo " Forward Hostname/IP: ${LAN_IP:-<LXC-IP>}"
|
||
echo " Forward Port: $PORT (Scheme: http)"
|
||
echo " SSL: Let's Encrypt aktivieren. Dann in $ENV_FILE APP_COOKIE_SECURE=true"
|
||
echo " setzen und 'systemctl restart $APP_SLUG'. (Uploads: client_max_body_size >= 20M.)"
|
||
echo
|
||
echo " Logs: journalctl -u $APP_SLUG -f"
|
||
echo " Neustart: systemctl restart $APP_SLUG"
|
||
echo " Update: ./deploy/update.sh"
|