Files
certain/deploy/install.sh
T
GuidoandClaude Opus 4.8 7461fddb7a feat: Phase 5 – PDF/Bild-Beleg-Import (lokal, Tesseract-OCR)
Backend:
- services/ocr: PDF-Text via PyMuPDF, OCR (Tesseract) für Scans/Bilder,
  heuristischer Parser (Datum + Betrag je Zeile, CH/DE/US-Beträge).
- routers/documents: /documents/preview (Upload -> Text -> Kandidaten ->
  gemeinsame Prüf-/Buchungslogik des CSV-Imports). csv_import refaktoriert:
  detect_rows() von CSV und Beleg gemeinsam genutzt.
- requirements: PyMuPDF, pytesseract, pillow; install.sh installiert tesseract-ocr
  (+ deu). pytest: Heuristik-Parser und PDF-Vorschau.

Frontend:
- Import-Seite: CSV/PDF-Umschalter; PDF-Modus mit Beleg-Auslesen, extrahiertem
  Text zur Kontrolle und Best-Effort-Hinweis; gleiche Prüfansicht + Buchen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 20:25:32 +02:00

217 lines
8.2 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"
# Der Dienst läuft als EIGENTÜMER des Repos. Wer als root nach /root klont, für den
# läuft der Dienst als root – nur so ist /root/... für den Dienst überhaupt
# zugänglich (ein eigener Dienst-Benutzer käme sonst nicht an Code/venv unter /root).
SERVICE_USER="$(stat -c '%U' "$REPO_DIR" 2>/dev/null || echo root)"
SERVICE_GROUP="$(stat -c '%G' "$REPO_DIR" 2>/dev/null || echo root)"
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 \
tesseract-ocr tesseract-ocr-deu
# --- 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
# --- Verzeichnisse (Dienst läuft als Repo-Eigentümer, s. o.) ----------------
info "Dienst läuft als Benutzer '$SERVICE_USER' (Eigentümer des Repos)."
mkdir -p "$DATA_DIR" "$ETC_DIR"
chown -R "$SERVICE_USER:$SERVICE_GROUP" "$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_GROUP
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"