From 2379f4ac9f6fd4dd73365b5def7cdca999aa6107 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Fri, 28 Aug 2026 16:44:56 +0200 Subject: [PATCH] @ Skripte nach bin/ und lib/ umstrukturiert + WSL-Log-Analyse Alle .bat-Skripte liegen jetzt in bin/, alle .py in lib/, .env(.example) in bin/. setenv.bat setzt IWZ_* (inkl. IWZ_ENV) und PYTHONPATH; die Launcher rufen setenv, aktivieren die .venv falls vorhanden und starten das zugehoerige lib/-Skript. Die Python-Skripte lesen die .env ueber IWZ_ENV (Fallback bin/.env). Neu: Nutzungsanalyse per GoAccess (analyze.py/.bat) - laedt die IONOS-Access-Logs per SFTP (paramiko) und erzeugt einen HTML-Report. GoAccess laeuft ueber WSL (GOACCESS_MODE=wsl, Pfad-Uebersetzung nach /mnt/c). install_py.bat prueft WSL+GoAccess und zeigt sonst den Installationsweg. Doku in doc/analyse.md. Co-Authored-By: Claude Opus 4.8 (1M context) @ --- .env.example | 16 -- .gitignore | 7 + CLAUDE.md | 40 +++- bin/.env.example | 38 ++++ bin/activate_venv.bat | 20 ++ bin/analyze.bat | 26 +++ bin/deploy-lastdiff.bat | 26 +++ bin/deploy.bat | 29 +++ bin/get_cmd.bat | 7 + bin/install_py.bat | 54 +++++ bin/provide_previewlink.bat | 27 +++ bin/setenv.bat | 77 +++++++ deploy-lastdiff.bat | 10 - deploy.bat | 10 - doc/analyse.md | 146 ++++++++++++ lib/analyze.py | 226 +++++++++++++++++++ deploy.py => lib/deploy.py | 14 +- deploy_lastdiff.py => lib/deploy_lastdiff.py | 14 +- preview_link.py => lib/preview_link.py | 15 +- provide_previewlink.bat | 8 - requirements.txt | 9 + 21 files changed, 760 insertions(+), 59 deletions(-) delete mode 100644 .env.example create mode 100644 bin/.env.example create mode 100644 bin/activate_venv.bat create mode 100644 bin/analyze.bat create mode 100644 bin/deploy-lastdiff.bat create mode 100644 bin/deploy.bat create mode 100644 bin/get_cmd.bat create mode 100644 bin/install_py.bat create mode 100644 bin/provide_previewlink.bat create mode 100644 bin/setenv.bat delete mode 100644 deploy-lastdiff.bat delete mode 100644 deploy.bat create mode 100644 doc/analyse.md create mode 100644 lib/analyze.py rename deploy.py => lib/deploy.py (89%) rename deploy_lastdiff.py => lib/deploy_lastdiff.py (90%) rename preview_link.py => lib/preview_link.py (82%) delete mode 100644 provide_previewlink.bat create mode 100644 requirements.txt diff --git a/.env.example b/.env.example deleted file mode 100644 index 04aa06b..0000000 --- a/.env.example +++ /dev/null @@ -1,16 +0,0 @@ -# Kopieren nach .env und mit echten Werten befuellen. -# .env ist in .gitignore eingetragen und wird NICHT eingecheckt. - -# Storage Zone -> Access-Tab im Bunny.net Dashboard -BUNNY_STORAGE_ZONE=iwz-ferienwohnungen -BUNNY_STORAGE_HOST=storage.bunnycdn.com -BUNNY_STORAGE_PASSWORD= - -# Pull Zone -> CDN -> Security-Tab im Bunny.net Dashboard (Token Authentication) -BUNNY_PULL_ZONE_URL=https://iwz-ferienwohnungen.b-cdn.net -BUNNY_AUTH_KEY= - -# Account-API-Key fuer den CDN-Cache-Purge nach dem Deploy. -# Dashboard -> oben rechts auf den Namen -> Account -> API -> "API Key". -# ACHTUNG: nicht der BUNNY_AUTH_KEY oben, sondern der Account-weite API-Key! -BUNNY_API_KEY= diff --git a/.gitignore b/.gitignore index 3e2c5e0..dce103f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ resources/ .env .deploy_manifest.json __pycache__/ + +# WordPress-Export der alten Seite (nur temporär, enthält DB-Zugangsdaten im Klartext) +app816341166/ + +# Nutzungsanalyse (analyze.py): heruntergeladene Rohlogs + erzeugte Reports +logs/ +results/ diff --git a/CLAUDE.md b/CLAUDE.md index ff0c2f8..151f87a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,14 +14,35 @@ Drei Ferienwohnungen in Kaufbeuren-Hirschzell / Allgäu. - **Buchungskalender**: webplanner.de (externer Dienst, per Link eingebunden) - **Git**: eigene Gitea-Instanz (kein GitHub/GitLab, kein CI/CD-Workflow) - **Deploy**: manuell per `deploy.bat` oder `deploy.py` → `hugo --gc --minify` + Upload nach Bunny.net Storage Zone (per Bunny-API/FTP, analog zum Ansatz in `.github/workflows/deploy.yml`-Vorlage, aber lokal statt als GitHub Action) +- **Nutzungsanalyse**: Server-Log-Auswertung mit GoAccess (kein JS-Tracking, keine Cookies, kein Consent-Banner). Skript `analyze.bat` / `analyze.py` lädt die Access-Logs per SFTP und erzeugt einen HTML-Report. Siehe `doc/analyse.md`. + +## Hosting-Roadmap (Stand aktuell) + +- **Jetzt**: Deploy läuft auf **Bunny.net** – für das erste Ausrollen der Instanz zur Kontrolle und Absprache. Die vorhandene Bunny-Konfiguration (Storage Zone, Pull Zone, `.env`) ist in Ordnung und bleibt vorerst so. `deploy.bat` / `deploy.py` deployen weiterhin nach Bunny. +- **Später**: Die Instanz wird auf **IONOS-Webspace** ausgerollt (Deploy-Ziel wird dann umgestellt). +- **Nutzungsanalyse jetzt schon erprobbar**: Die Log-Analyse (`analyze.py`) wird bereits mit den **IONOS-Access-Logs** getestet (SFTP-Zugang zum IONOS `/logs`-Ordner), unabhängig davon, dass das Deploy-Ziel noch Bunny ist. Nach dem IONOS-Umzug bleibt der Analyse-Workflow gleich. ## Projektstruktur ``` iwz/ ├── hugo.toml # Konfiguration: baseURL, Menü, Kontaktdaten (Params) -├── deploy.bat # Windows: hugo build + wrangler deploy -├── deploy.py # Python-Alternative zum deploy.bat +├── requirements.txt # Python-Abhängigkeiten (paramiko für analyze) +├── bin/ # Alle .bat-Skripte (Einstiegspunkte) +│ ├── setenv.bat # setzt IWZ, IWZ_BIN, IWZ_LIB, IWZ_ENV ... + PYTHONPATH +│ ├── install_py.bat # legt .venv an + pip install -r requirements.txt +│ ├── get_cmd.bat # öffnet Shell mit gesetzter Umgebung +│ ├── deploy.bat # → lib/deploy.py (Vollupload nach Bunny) +│ ├── deploy-lastdiff.bat# → lib/deploy_lastdiff.py (nur Änderungen) +│ ├── analyze.bat # → lib/analyze.py (IONOS-Logs + GoAccess) +│ ├── provide_previewlink.bat # → lib/preview_link.py +│ ├── .env # Secrets (NICHT im Git; von setenv als IWZ_ENV gesetzt) +│ └── .env.example # Vorlage zum Kopieren nach bin/.env +├── lib/ # Alle .py-Skripte (von den .bat aufgerufen) +│ ├── deploy.py # Hugo-Build + FTP-Upload nach Bunny Storage Zone +│ ├── deploy_lastdiff.py # nur geänderte Dateien (Hash-Manifest) +│ ├── analyze.py # SFTP-Download der Logs + GoAccess-Report +│ └── preview_link.py # signierter Bunny-Vorschau-Link ├── content/ │ ├── _index.md # Startseite │ ├── preise.md # Preistabelle + Buchungsbedingungen @@ -94,8 +115,15 @@ hugo --gc --minify # danach: Upload von ./public zur Bunny.net Storage Zone # (Storage Zone Name + API-Key/Storage-Passwort als Secrets/env, nicht im Git) -# Bei jeder Änderung: -deploy.bat -# oder: -python deploy.py +# Bei jeder Änderung (Skripte liegen jetzt in bin/, Python in lib/): +bin\deploy.bat # Vollupload +# oder nur geänderte Dateien: +bin\deploy-lastdiff.bat +# oder direkt: +python lib\deploy.py ``` + +Die `.bat`-Skripte in `bin/` rufen `setenv.bat` auf (setzt `IWZ_*` und +`PYTHONPATH`), wechseln ins Projekt-Root und starten das zugehörige Skript in +`lib/`. Die Secrets liegen in `bin/.env` (von `setenv.bat` als `IWZ_ENV` +gesetzt; die Python-Skripte lesen diesen Pfad, sonst Fallback auf `bin/.env`). diff --git a/bin/.env.example b/bin/.env.example new file mode 100644 index 0000000..cdc933e --- /dev/null +++ b/bin/.env.example @@ -0,0 +1,38 @@ +# Kopieren nach .env und mit echten Werten befuellen. +# .env ist in .gitignore eingetragen und wird NICHT eingecheckt. + +# Storage Zone -> Access-Tab im Bunny.net Dashboard +BUNNY_STORAGE_ZONE=iwz-ferienwohnungen +BUNNY_STORAGE_HOST=storage.bunnycdn.com +BUNNY_STORAGE_PASSWORD= + +# Pull Zone -> CDN -> Security-Tab im Bunny.net Dashboard (Token Authentication) +BUNNY_PULL_ZONE_URL=https://iwz-ferienwohnungen.b-cdn.net +BUNNY_AUTH_KEY= + +# Account-API-Key fuer den CDN-Cache-Purge nach dem Deploy. +# Dashboard -> oben rechts auf den Namen -> Account -> API -> "API Key". +# ACHTUNG: nicht der BUNNY_AUTH_KEY oben, sondern der Account-weite API-Key! +BUNNY_API_KEY= + +# --- Nutzungsanalyse (analyze.py) ------------------------------------------ +# SFTP-Zugang zu IONOS (gleiche Zugangsdaten wie im IONOS-Kundenbereich / SFTP). +IONOS_SFTP_HOST= +IONOS_SFTP_PORT=22 +IONOS_SFTP_USER= +IONOS_SFTP_PASSWORD= +# Ordner mit den Access-Logs auf dem Server (im Screenshot: /logs). +IONOS_LOGS_DIR=/logs + +# GoAccess-Aufruf: +# GOACCESS_MODE=wsl -> GoAccess laeuft in der WSL (empfohlen, Standard). +# Installation: bin\install_py.bat prueft WSL+GoAccess +# und zeigt sonst die Installationsbefehle. +# GOACCESS_MODE=exe -> nativer Windows-Build; dann GOACCESS_EXE auf die +# goaccess.exe setzen (Download: https://goaccess.io/download). +GOACCESS_MODE=wsl +GOACCESS_EXE=goaccess + +# Apache-Logformat. IONOS nutzt i.d.R. das Combined Log Format. +# Falls der Report leer bleibt -> siehe doc/analyse.md (Format anpassen). +LOG_FORMAT=COMBINED diff --git a/bin/activate_venv.bat b/bin/activate_venv.bat new file mode 100644 index 0000000..c6b5564 --- /dev/null +++ b/bin/activate_venv.bat @@ -0,0 +1,20 @@ +@echo off +REM ================================================================ +REM SPS_SKEL - Python Virtual Environment aktivieren +REM ================================================================ + +call "%~dp0setenv.bat" + +if not exist "%IWZ%\.venv" ( + echo FEHLER: Virtual environment nicht gefunden. + echo Bitte zuerst bin\install_py.bat ausfuehren. + exit /b 1 +) + +call "%IWZ%\.venv\Scripts\activate.bat" +echo Virtuelle Umgebung aktiviert. +echo Python-Version: +python --version +echo. +echo Installierte Pakete: +pip list diff --git a/bin/analyze.bat b/bin/analyze.bat new file mode 100644 index 0000000..751ce88 --- /dev/null +++ b/bin/analyze.bat @@ -0,0 +1,26 @@ +@echo off +REM ================================================================ +REM IWZ Ferienwohnungen - Nutzungsanalyse (IONOS-Logs + GoAccess) +REM ================================================================ +REM Aufruf: +REM bin\analyze.bat Logs laden + Report bauen + oeffnen +REM bin\analyze.bat --no-download nur neu auswerten (schon geladene Logs) +REM ================================================================ +setlocal + +call "%~dp0setenv.bat" >nul + +REM Virtuelle Umgebung nutzen, falls vorhanden (paramiko liegt dort) +if exist "%IWZ%\.venv\Scripts\activate.bat" ( + call "%IWZ%\.venv\Scripts\activate.bat" +) + +set "PY=py" +py --version >nul 2>&1 || set "PY=python" + +cd /d "%IWZ%" +"%PY%" "%IWZ_LIB%\analyze.py" %* +set "RC=%ERRORLEVEL%" + +if not "%RC%"=="0" pause +endlocal & exit /b %RC% diff --git a/bin/deploy-lastdiff.bat b/bin/deploy-lastdiff.bat new file mode 100644 index 0000000..934ffd7 --- /dev/null +++ b/bin/deploy-lastdiff.bat @@ -0,0 +1,26 @@ +@echo off +REM ================================================================ +REM IWZ Ferienwohnungen - Deploy (nur geaenderte Dateien) +REM ================================================================ +setlocal + +call "%~dp0setenv.bat" >nul + +REM Virtuelle Umgebung nutzen, falls vorhanden (paramiko etc. liegen dort) +if exist "%IWZ%\.venv\Scripts\activate.bat" ( + call "%IWZ%\.venv\Scripts\activate.bat" +) + +set "PY=py" +py --version >nul 2>&1 || set "PY=python" + +echo. +echo === IWZ Ferienwohnungen - Deploy (nur Aenderungen) === +echo. + +cd /d "%IWZ%" +"%PY%" "%IWZ_LIB%\deploy_lastdiff.py" %* +set "RC=%ERRORLEVEL%" + +if not "%RC%"=="0" pause +endlocal & exit /b %RC% diff --git a/bin/deploy.bat b/bin/deploy.bat new file mode 100644 index 0000000..c357189 --- /dev/null +++ b/bin/deploy.bat @@ -0,0 +1,29 @@ +@echo off +REM ================================================================ +REM IWZ Ferienwohnungen - Deploy (Vollupload nach Bunny.net) +REM ================================================================ +setlocal + +REM Umgebung setzen (IWZ, IWZ_LIB, IWZ_ENV, PYTHONPATH ...) +call "%~dp0setenv.bat" >nul + +REM Virtuelle Umgebung nutzen, falls vorhanden (paramiko etc. liegen dort) +if exist "%IWZ%\.venv\Scripts\activate.bat" ( + call "%IWZ%\.venv\Scripts\activate.bat" +) + +REM Python-Interpreter bestimmen (py-Launcher bevorzugt, sonst python) +set "PY=py" +py --version >nul 2>&1 || set "PY=python" + +echo. +echo === IWZ Ferienwohnungen - Deploy === +echo. + +REM Aus dem Projekt-Root heraus bauen (hugo + public liegen dort) +cd /d "%IWZ%" +"%PY%" "%IWZ_LIB%\deploy.py" %* +set "RC=%ERRORLEVEL%" + +if not "%RC%"=="0" pause +endlocal & exit /b %RC% diff --git a/bin/get_cmd.bat b/bin/get_cmd.bat new file mode 100644 index 0000000..b70e540 --- /dev/null +++ b/bin/get_cmd.bat @@ -0,0 +1,7 @@ +@echo off +REM ================================================================ +REM IWZ Ferienwohnungen - Shell mit gesetzten Umgebungsvariablen oeffnen +REM ================================================================ + +call "%~dp0setenv.bat" +start cmd /k "echo IWZ Umgebung aktiv. && echo IWZ=%IWZ%" diff --git a/bin/install_py.bat b/bin/install_py.bat new file mode 100644 index 0000000..9bc84c8 --- /dev/null +++ b/bin/install_py.bat @@ -0,0 +1,54 @@ +@echo off +REM ================================================================ +REM IWZ - Python Virtual Environment einrichten +REM ================================================================ + +call "%~dp0setenv.bat" + +if not exist "%IWZ%\.venv" ( + echo Initialisiere Python virtual environment... + py -m venv "%IWZ%\.venv" --upgrade-deps + echo Erfolgreich. + + call "%IWZ%\.venv\Scripts\activate.bat" + echo Installiere erforderliche Python Packages... + pip install -r "%IWZ%\requirements.txt" -q + echo Erfolgreich. + deactivate +) else ( + echo Erforderliche Python Packages bereits installiert! +) + +echo. +echo ================================================================ +echo Pruefe Nutzungsanalyse-Voraussetzungen (WSL + GoAccess) +echo ================================================================ + +REM 1) Ist WSL verfuegbar? +where wsl >nul 2>&1 +if errorlevel 1 ( + echo [FEHLT] WSL ist nicht installiert. + echo So einrichten ^(einmalig, PowerShell als Administrator^): + echo wsl --install + echo Danach Windows neu starten und die Ubuntu-Ersteinrichtung + echo ^(Benutzername/Passwort^) durchlaufen. Anschliessend GoAccess + echo wie unten installieren. + goto :wsl_done +) +echo [OK] WSL gefunden. + +REM 2) Ist GoAccess in der WSL installiert? +wsl -e bash -lc "command -v goaccess >/dev/null 2>&1" +if errorlevel 1 ( + echo [FEHLT] GoAccess ist in der WSL nicht installiert. + echo So installieren: + echo wsl -e sudo apt update + echo wsl -e sudo apt install -y goaccess + echo ^(fragt einmalig nach dem WSL-sudo-Passwort^) +) else ( + for /f "delims=" %%V in ('wsl -e bash -lc "goaccess --version ^| head -1"') do set "GA_VER=%%V" + echo [OK] GoAccess in der WSL: %GA_VER% +) + +:wsl_done +echo ================================================================ diff --git a/bin/provide_previewlink.bat b/bin/provide_previewlink.bat new file mode 100644 index 0000000..51bee34 --- /dev/null +++ b/bin/provide_previewlink.bat @@ -0,0 +1,27 @@ +@echo off +REM ================================================================ +REM Ferienwohnungen Wolframm - Vorschau-Link erzeugen +REM ================================================================ +setlocal + +call "%~dp0setenv.bat" >nul + +REM Virtuelle Umgebung nutzen, falls vorhanden +if exist "%IWZ%\.venv\Scripts\activate.bat" ( + call "%IWZ%\.venv\Scripts\activate.bat" +) + +set "PY=py" +py --version >nul 2>&1 || set "PY=python" + +echo. +echo === Ferienwohnungen Wolframm - Vorschau-Link erzeugen === + +cd /d "%IWZ%" +"%PY%" "%IWZ_LIB%\preview_link.py" %* + +echo. +echo Link oben markieren und mit Strg+C kopieren. +echo. +endlocal +cmd /k diff --git a/bin/setenv.bat b/bin/setenv.bat new file mode 100644 index 0000000..b5c2437 --- /dev/null +++ b/bin/setenv.bat @@ -0,0 +1,77 @@ +@echo off + +REM ================================================================ +REM IWZ Ferienwohnungen - Umgebungsvariablen Setup +REM ================================================================ + +if defined IWZ_BIN ( + echo IWZ Umgebung bereits gesetzt. + goto :show +) + +echo Setting up environment variables for IWZ ... + +REM Basis-Projektpfad (Ordner ueber bin\) +set "IWZ=%~dp0.." +if "%IWZ:~-6%"=="bin\.." set "IWZ=%IWZ:~0,-6%" +if "%IWZ:~-1%"=="\" set "IWZ=%IWZ:~0,-1%" + +REM Pfade fuer verschiedene Komponenten +set "IWZ_BIN=%IWZ%\bin" +set "IWZ_LIB=%IWZ%\lib" +set "IWZ_DOC=%IWZ%\doc" +set "IWZ_CONTENT=%IWZ%\content" +set "IWZ_LAYOUTS=%IWZ%\layouts" +set "IWZ_STATIC=%IWZ%\static" +set "IWZ_PUBLIC=%IWZ%\public" +set "IWZ_LOGS=%IWZ%\logs" +set "IWZ_RESULTS=%IWZ%\results" + +REM Pfad zur .env (liegt jetzt in bin\). Die Python-Skripte lesen IWZ_ENV. +set "IWZ_ENV=%IWZ_BIN%\.env" + +REM Python-Pfad erweitern (nur wenn noch nicht vorhanden) +echo %PYTHONPATH% | find /i "%IWZ_LIB%" >nul +if errorlevel 1 ( + set "PYTHONPATH=%IWZ_LIB%;%PYTHONPATH%" +) + +REM Ordner erstellen falls sie nicht existieren +if not exist "%IWZ_BIN%" mkdir "%IWZ_BIN%" +if not exist "%IWZ_LIB%" mkdir "%IWZ_LIB%" +if not exist "%IWZ_LOGS%" mkdir "%IWZ_LOGS%" +if not exist "%IWZ_RESULTS%" mkdir "%IWZ_RESULTS%" + +REM Optional dauerhaft in der Registry setzen: bin\setenv.bat --persist +if /i "%1"=="--persist" ( + echo Setze Umgebungsvariablen dauerhaft in der Registry ^(setx^)... + setx IWZ "%IWZ%" >nul + setx IWZ_BIN "%IWZ_BIN%" >nul + setx IWZ_LIB "%IWZ_LIB%" >nul + setx IWZ_ENV "%IWZ_ENV%" >nul + echo Dauerhaft gesetzt. Gilt fuer alle neuen Prozesse. +) + +REM Umgebungsvariablen anzeigen +:show +echo. +echo ================================================================ +echo IWZ ENVIRONMENT SETUP COMPLETE +echo ================================================================ +echo IWZ = %IWZ% +echo IWZ_BIN = %IWZ_BIN% +echo IWZ_LIB = %IWZ_LIB% +echo IWZ_DOC = %IWZ_DOC% +echo IWZ_CONTENT = %IWZ_CONTENT% +echo IWZ_LAYOUTS = %IWZ_LAYOUTS% +echo IWZ_STATIC = %IWZ_STATIC% +echo IWZ_PUBLIC = %IWZ_PUBLIC% +echo IWZ_LOGS = %IWZ_LOGS% +echo IWZ_RESULTS = %IWZ_RESULTS% +echo IWZ_ENV = %IWZ_ENV% +echo PYTHONPATH = %PYTHONPATH% +echo ================================================================ +echo. + +REM Optionally keep window open +if "%1"=="--keep-open" pause diff --git a/deploy-lastdiff.bat b/deploy-lastdiff.bat deleted file mode 100644 index b9282a2..0000000 --- a/deploy-lastdiff.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -echo. -echo === IWZ Ferienwohnungen - Deploy (nur Änderungen) === -echo. -python deploy_lastdiff.py -if %errorlevel% neq 0 ( - pause - exit /b 1 -) -pause diff --git a/deploy.bat b/deploy.bat deleted file mode 100644 index c02cfe4..0000000 --- a/deploy.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -echo. -echo === IWZ Ferienwohnungen - Deploy === -echo. -python deploy.py -if %errorlevel% neq 0 ( - pause - exit /b 1 -) -pause diff --git a/doc/analyse.md b/doc/analyse.md new file mode 100644 index 0000000..b21a575 --- /dev/null +++ b/doc/analyse.md @@ -0,0 +1,146 @@ +# Nutzungsanalyse der Website (Server-Logs + GoAccess) + +Die neue Hugo-Seite verzichtet bewusst auf JavaScript-Tracking (kein Google +Analytics, keine Cookies, kein Consent-Banner nötig). Stattdessen werten wir +die **Apache-Access-Logs** aus, die IONOS ohnehin auf dem Server schreibt. + +Ablauf des Skripts `analyze.py`: + +1. Verbindet sich per **SFTP** mit IONOS und lädt alle `access.log*`-Dateien + aus dem `/logs`-Ordner nach `./logs/` herunter. +2. Entpackt die rotierten `.gz`-Logs und fügt alles zu einer Datei zusammen. +3. Lässt **GoAccess** darüberlaufen und erzeugt einen HTML-Report unter + `./results/report_JJJJ-MM-TT.html`, der automatisch im Browser geöffnet wird. + +`logs/` und `results/` stehen in `.gitignore` und werden nicht eingecheckt. + +--- + +## Einmalige Einrichtung + +### 1. Python-Abhängigkeit (paramiko) + +```powershell +pip install -r requirements.txt +``` + +### 2. GoAccess (über WSL – empfohlen) + +GoAccess wird offiziell nur für Linux/macOS gebaut und ist in keinem +Windows-Paketmanager verfügbar. Unter Windows ist der zuverlässigste Weg die +**WSL** (Windows Subsystem for Linux). `bin\install_py.bat` prüft automatisch, +ob WSL und GoAccess vorhanden sind, und zeigt sonst die passenden Befehle. + +**WSL installieren** (falls noch nicht vorhanden, PowerShell als Administrator): + +```powershell +wsl --install +``` + +Danach Windows neu starten und die Ubuntu-Ersteinrichtung (Benutzer/Passwort) +durchlaufen. + +**GoAccess in der WSL installieren:** + +```powershell +wsl -e sudo apt update +wsl -e sudo apt install -y goaccess +``` + +In der `.env` steht dafür (Standard): + +```text +GOACCESS_MODE=wsl +``` + +Das Skript ruft GoAccess dann per `wsl goaccess ...` auf und übersetzt die +Windows-Pfade automatisch nach `/mnt/c/...`. + +> **Alternative – nativer Windows-Build:** Falls du kein WSL nutzen willst, +> lade den Cygwin-Build von , entpacke ihn (z. B. +> nach `C:\tools\goaccess\`) und setze in der `.env`: +> +> ```text +> GOACCESS_MODE=exe +> GOACCESS_EXE=C:\tools\goaccess\goaccess.exe +> ``` + +### 3. IONOS-SFTP-Zugang in `.env` eintragen + +Die Zugangsdaten stehen im IONOS-Kundenbereich unter **Hosting → SFTP/SSH**. +Es sind dieselben, mit denen der `/logs`-Ordner erreichbar ist. + +```text +IONOS_SFTP_HOST= +IONOS_SFTP_PORT=22 +IONOS_SFTP_USER= +IONOS_SFTP_PASSWORD= +IONOS_LOGS_DIR=/logs +``` + +> Die `.env` steht in `.gitignore` und wird **nicht** eingecheckt. + +--- + +## Nutzung + +```powershell +# Logs neu laden, Report bauen und im Browser öffnen: +analyze.bat +# oder: +python analyze.py + +# Nur neu auswerten (ohne erneuten Download der schon geladenen Logs): +python analyze.py --no-download +``` + +Beim wiederholten Lauf werden bereits geladene, rotierte Logs übersprungen +(gleiche Dateigröße) – nur das aktuell wachsende Log wird neu geholt. + +--- + +## Was im Report steht + +GoAccess zeigt u. a.: + +- **Eindeutige Besucher** und Seitenaufrufe pro Tag +- **Meistbesuchte Seiten** (welche Ferienwohnung interessiert am meisten?) +- **Referrer** (woher kommen die Besucher – Google, direkte Eingabe, Links) +- **Länder / Sprachen**, Browser, Betriebssysteme, Geräte +- **HTTP-Statuscodes** (404-Fehler = tote Links finden) +- **Traffic-Volumen** + +Aufrufe von Suchmaschinen-Bots werden herausgefiltert (`--ignore-crawlers`), +IP-Adressen im Report anonymisiert (`--anonymize-ip`). + +--- + +## DSGVO-Hinweis + +Diese Auswertung passiert **nur lokal auf deinem Rechner** aus den ohnehin +anfallenden Server-Logs. Es wird nichts an Dritte übertragen und kein +Tracking-Code beim Besucher ausgeführt. Server-Logs mit IP-Adressen dürfen zu +Sicherheits-/Betriebszwecken vorübergehend gespeichert werden; im GoAccess- +Report sind die IPs zusätzlich anonymisiert. Ein Cookie-Consent-Banner ist für +diese Art der Auswertung **nicht** erforderlich. + +--- + +## Fehlerbehebung + +**Report bleibt leer / „No data".** +Dann passt das Log-Format nicht. IONOS nutzt meist das *Combined Log Format* +(`LOG_FORMAT=COMBINED`). Wirf einen Blick in die erste Zeile von +`logs/access.log.current`: + +- Beginnt sie mit einer IP und enthält am Ende `"...Referrer" "...User-Agent"` + → `COMBINED` ist korrekt. +- Fehlen Referrer/User-Agent → `LOG_FORMAT=COMMON`. +- Steht der Vhost-Name vor der IP → `LOG_FORMAT=VCOMBINED`. + +Alternativ ein eigenes Format als GoAccess-`--log-format`-String in `.env` +hinterlegen (siehe ). + +**`paramiko`-Authentifizierungsfehler.** +SFTP-Host/-User/-Passwort im IONOS-Kundenbereich gegenprüfen. Der SFTP-Benutzer +ist nicht zwingend identisch mit dem Login des Kundenbereichs. diff --git a/lib/analyze.py b/lib/analyze.py new file mode 100644 index 0000000..c719cea --- /dev/null +++ b/lib/analyze.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +""" +IWZ Ferienwohnungen - Nutzungsanalyse + +Laedt die Apache-Access-Logs per SFTP von IONOS herunter, entpackt die +rotierten .gz-Dateien und erzeugt mit GoAccess einen HTML-Report. + +Kein JavaScript-Tracking, keine Cookies beim Besucher -> DSGVO-freundlich. +Die Auswertung passiert rein aus den Server-Logs. + +Voraussetzungen: + - pip install -r requirements.txt (paramiko) + - GoAccess Windows-Build (Pfad in .env: GOACCESS_EXE) + - .env mit IONOS_SFTP_* befuellt (siehe .env.example) + +Aufruf: + python analyze.py # Logs laden + Report bauen + oeffnen + python analyze.py --no-download # nur neu auswerten (schon geladene Logs) +""" + +import gzip +import os +import shutil +import stat +import subprocess +import sys +import webbrowser +from datetime import date +from pathlib import Path + +try: + import paramiko +except ImportError: + print("FEHLER: paramiko fehlt. Bitte 'pip install -r requirements.txt' ausfuehren.") + sys.exit(1) + +PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/ +LOGS_DIR = PROJECT_ROOT / "logs" # heruntergeladene Rohlogs (in .gitignore) +RESULTS_DIR = PROJECT_ROOT / "results" # erzeugte Reports (in .gitignore) +SFTP_PORT = 22 + + +def env_file(): + """Pfad zur .env: bevorzugt IWZ_ENV (von setenv.bat), sonst bin/.env.""" + from_var = os.environ.get("IWZ_ENV") + if from_var: + return Path(from_var) + return PROJECT_ROOT / "bin" / ".env" + +# Nur diese Dateien aus dem IONOS /logs-Ordner interessieren uns. +# Alles andere (sftp.log, traffic.db, forensic/, info.*) wird ignoriert. +ACCESS_LOG_PREFIX = "access.log" + + +def load_env(path): + env = {} + if not path.exists(): + return env + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + env[key.strip()] = value.strip() + return env + + +def download_access_logs(host, port, user, password, remote_dir): + """Laedt alle access.log*-Dateien aus dem IONOS /logs-Ordner nach LOGS_DIR.""" + print(f"\n-> Verbinde per SFTP mit {host}:{port} ...") + transport = paramiko.Transport((host, port)) + try: + transport.connect(username=user, password=password) + sftp = paramiko.SFTPClient.from_transport(transport) + sftp.chdir(remote_dir) + + LOGS_DIR.mkdir(exist_ok=True) + count = 0 + for attr in sftp.listdir_attr("."): + name = attr.filename + if not name.startswith(ACCESS_LOG_PREFIX): + continue + if stat.S_ISDIR(attr.st_mode): + continue + local_path = LOGS_DIR / name + # Schon vorhanden und gleich gross? -> ueberspringen (rotierte Logs + # aendern sich nicht mehr; nur .current / aktueller Tag waechst). + if (local_path.exists() + and local_path.stat().st_size == attr.st_size + and not name.endswith(".current")): + continue + print(f" lade {name} ({attr.st_size} Bytes)") + sftp.get(name, str(local_path)) + count += 1 + sftp.close() + print(f" {count} Datei(en) geladen/aktualisiert.") + finally: + transport.close() + + +def collect_log_lines(): + """Fuehrt alle access.log*-Dateien (auch .gz) zu einem kombinierten Log zusammen. + + Gibt den Pfad zur kombinierten Klartext-Logdatei zurueck. + """ + combined = LOGS_DIR / "_combined.log" + sources = sorted( + p for p in LOGS_DIR.iterdir() + if p.is_file() + and p.name.startswith(ACCESS_LOG_PREFIX) + and p.name != combined.name + ) + if not sources: + print("\nFEHLER: keine access.log*-Dateien in ./logs/ gefunden.") + print("Erst 'python analyze.py' (ohne --no-download) ausfuehren.") + sys.exit(1) + + print(f"\n-> Fuehre {len(sources)} Logdatei(en) zusammen ...") + with open(combined, "wb") as out: + for src in sources: + if src.suffix == ".gz": + with gzip.open(src, "rb") as f: + shutil.copyfileobj(f, out) + else: + with open(src, "rb") as f: + shutil.copyfileobj(f, out) + return combined + + +def to_wsl_path(win_path): + """Uebersetzt einen Windows-Pfad (C:\\...) in einen WSL-Pfad (/mnt/c/...).""" + p = Path(win_path).resolve() + drive = p.drive.rstrip(":").lower() # "C:" -> "c" + rest = p.as_posix()[len(p.drive):] # Rest ohne Laufwerk, mit / + return f"/mnt/{drive}{rest}" + + +def goaccess_args(log_format): + """Gemeinsame GoAccess-Optionen (ohne Ein-/Ausgabedateien).""" + return [ + "--log-format=" + log_format, + "--anonymize-ip", # DSGVO: IPs im Report anonymisieren + "--ignore-crawlers", # Bots aus den Besucherzahlen filtern + "--html-report-title=IWZ Ferienwohnungen - Zugriffsstatistik", + ] + + +def run_goaccess(mode, goaccess_exe, log_file, report_file, log_format): + """Ruft GoAccess auf und erzeugt einen eigenstaendigen HTML-Report. + + mode="wsl": GoAccess laeuft in der WSL (Standard), Pfade werden nach + /mnt/c/... uebersetzt. + mode="exe": nativer Windows-Build unter GOACCESS_EXE. + """ + print(f"\n-> GoAccess ({mode}) -> {report_file.name}") + + if mode == "wsl": + cmd = [ + "wsl", "goaccess", + to_wsl_path(log_file), + "-o", to_wsl_path(report_file), + ] + goaccess_args(log_format) + not_found_hint = ( + "GoAccess ist in der WSL nicht installiert (oder WSL fehlt).\n" + " In der WSL installieren: wsl -e sudo apt update ^&^& " + "sudo apt install -y goaccess\n" + "Siehe auch bin/install_py.bat (prueft WSL + GoAccess) und doc/analyse.md." + ) + else: + cmd = [ + goaccess_exe, + str(log_file), + "-o", str(report_file), + ] + goaccess_args(log_format) + not_found_hint = ( + f"GoAccess-EXE nicht gefunden ('{goaccess_exe}').\n" + " Windows-Build laden: https://goaccess.io/download, entpacken,\n" + " dann GOACCESS_EXE in bin/.env auf die goaccess.exe setzen." + ) + + try: + result = subprocess.run(cmd) + except FileNotFoundError: + print("\nFEHLER: GoAccess konnte nicht gestartet werden.") + print(not_found_hint) + sys.exit(1) + if result.returncode != 0: + print("\nFEHLER: GoAccess fehlgeschlagen.") + print("Pruefe die GoAccess-Installation und das Log-Format (LOG_FORMAT).") + sys.exit(1) + + +if __name__ == "__main__": + print("=== IWZ Nutzungsanalyse ===") + no_download = "--no-download" in sys.argv + + env = {**load_env(env_file()), **os.environ} + host = env.get("IONOS_SFTP_HOST") + port = int(env.get("IONOS_SFTP_PORT", SFTP_PORT)) + user = env.get("IONOS_SFTP_USER") + password = env.get("IONOS_SFTP_PASSWORD") + remote_dir = env.get("IONOS_LOGS_DIR", "/logs") + # GoAccess-Aufruf: "wsl" (Standard) oder "exe" (nativer Windows-Build). + goaccess_mode = env.get("GOACCESS_MODE", "wsl").strip().lower() + goaccess_exe = env.get("GOACCESS_EXE", "goaccess") + # IONOS liefert i.d.R. Apache "Combined Log Format". Bei anderen + # Formaten hier anpassen (siehe doc/analyse.md). + log_format = env.get("LOG_FORMAT", "COMBINED") + + if not no_download: + if not host or not user or not password: + print("\nFEHLER: IONOS_SFTP_HOST / _USER / _PASSWORD fehlen in bin/.env.") + print("Siehe bin/.env.example. Oder mit --no-download nur neu auswerten.") + sys.exit(1) + download_access_logs(host, port, user, password, remote_dir) + else: + print("\n-> Download uebersprungen (--no-download).") + + combined = collect_log_lines() + + RESULTS_DIR.mkdir(exist_ok=True) + report_file = RESULTS_DIR / f"report_{date.today().isoformat()}.html" + run_goaccess(goaccess_mode, goaccess_exe, combined, report_file, log_format) + + print(f"\nFertig! Report: {report_file}") + webbrowser.open(report_file.as_uri()) diff --git a/deploy.py b/lib/deploy.py similarity index 89% rename from deploy.py rename to lib/deploy.py index e4e6212..8f42722 100644 --- a/deploy.py +++ b/lib/deploy.py @@ -13,11 +13,19 @@ import urllib.parse import urllib.request from pathlib import Path -PROJECT_ROOT = Path(__file__).resolve().parent +PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/ PUBLIC_DIR = PROJECT_ROOT / "public" FTP_PORT = 21 +def env_file(): + """Pfad zur .env: bevorzugt IWZ_ENV (von setenv.bat), sonst bin/.env.""" + from_var = os.environ.get("IWZ_ENV") + if from_var: + return Path(from_var) + return PROJECT_ROOT / "bin" / ".env" + + def load_env(path): env = {} if not path.exists(): @@ -99,7 +107,7 @@ def purge_cdn_cache(pull_zone_url, api_key): if __name__ == "__main__": print("=== IWZ Deploy ===") - env = {**load_env(PROJECT_ROOT / ".env"), **os.environ} + env = {**load_env(env_file()), **os.environ} zone = env.get("BUNNY_STORAGE_ZONE") host = env.get("BUNNY_STORAGE_HOST", "storage.bunnycdn.com") password = env.get("BUNNY_STORAGE_PASSWORD") @@ -108,7 +116,7 @@ if __name__ == "__main__": if not zone or not password: print("\nFEHLER: BUNNY_STORAGE_ZONE / BUNNY_STORAGE_PASSWORD fehlen.") - print("Siehe .env.example -> .env kopieren und befuellen.") + print("Siehe bin/.env.example -> bin/.env kopieren und befuellen.") sys.exit(1) hugo_build() diff --git a/deploy_lastdiff.py b/lib/deploy_lastdiff.py similarity index 90% rename from deploy_lastdiff.py rename to lib/deploy_lastdiff.py index 643deb3..334cb4d 100644 --- a/deploy_lastdiff.py +++ b/lib/deploy_lastdiff.py @@ -14,12 +14,20 @@ import subprocess import sys from pathlib import Path -PROJECT_ROOT = Path(__file__).resolve().parent +PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/ PUBLIC_DIR = PROJECT_ROOT / "public" MANIFEST_PATH = PROJECT_ROOT / ".deploy_manifest.json" FTP_PORT = 21 +def env_file(): + """Pfad zur .env: bevorzugt IWZ_ENV (von setenv.bat), sonst bin/.env.""" + from_var = os.environ.get("IWZ_ENV") + if from_var: + return Path(from_var) + return PROJECT_ROOT / "bin" / ".env" + + def load_env(path): env = {} if not path.exists(): @@ -124,14 +132,14 @@ def deploy_to_bunny(host, zone, password, changed, deleted): if __name__ == "__main__": print("=== IWZ Deploy (nur Änderungen) ===") - env = {**load_env(PROJECT_ROOT / ".env"), **os.environ} + env = {**load_env(env_file()), **os.environ} zone = env.get("BUNNY_STORAGE_ZONE") host = env.get("BUNNY_STORAGE_HOST", "storage.bunnycdn.com") password = env.get("BUNNY_STORAGE_PASSWORD") if not zone or not password: print("\nFEHLER: BUNNY_STORAGE_ZONE / BUNNY_STORAGE_PASSWORD fehlen.") - print("Siehe .env.example -> .env kopieren und befuellen.") + print("Siehe bin/.env.example -> bin/.env kopieren und befuellen.") sys.exit(1) hugo_build() diff --git a/preview_link.py b/lib/preview_link.py similarity index 82% rename from preview_link.py rename to lib/preview_link.py index 6969bf0..cffd7b2 100644 --- a/preview_link.py +++ b/lib/preview_link.py @@ -6,15 +6,24 @@ fuer die per Bunny Token Authentication geschuetzte Pull Zone. import base64 import hashlib +import os import sys import time from pathlib import Path -PROJECT_ROOT = Path(__file__).resolve().parent +PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/ GUELTIGKEIT_SEKUNDEN = 7 * 24 * 60 * 60 # 7 Tage URL_PATH = "/" +def env_file(): + """Pfad zur .env: bevorzugt IWZ_ENV (von setenv.bat), sonst bin/.env.""" + from_var = os.environ.get("IWZ_ENV") + if from_var: + return Path(from_var) + return PROJECT_ROOT / "bin" / ".env" + + def load_env(path): env = {} if not path.exists(): @@ -36,12 +45,12 @@ def build_signed_url(pull_zone_url, auth_key, url_path, expires): if __name__ == "__main__": - env = load_env(PROJECT_ROOT / ".env") + env = load_env(env_file()) pull_zone_url = env.get("BUNNY_PULL_ZONE_URL", "").rstrip("/") auth_key = env.get("BUNNY_AUTH_KEY") if not pull_zone_url or not auth_key: - print("\nFEHLER: BUNNY_PULL_ZONE_URL / BUNNY_AUTH_KEY fehlen in .env.") + print("\nFEHLER: BUNNY_PULL_ZONE_URL / BUNNY_AUTH_KEY fehlen in bin/.env.") print("Siehe .env.example. Auth Key: Pull Zone -> Security -> Token Authentication.") sys.exit(1) diff --git a/provide_previewlink.bat b/provide_previewlink.bat deleted file mode 100644 index 1329017..0000000 --- a/provide_previewlink.bat +++ /dev/null @@ -1,8 +0,0 @@ -@echo off -echo. -echo === Ferienwohnungen Wolframm - Vorschau-Link erzeugen === -python preview_link.py -echo. -echo Link oben markieren und mit Strg+C kopieren. -echo. -cmd /k diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ec686f8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +# Python-Abhaengigkeiten fuer die lib/-Skripte. +# +# deploy.py / deploy_lastdiff.py / preview_link.py: +# nutzen ausschliesslich die Standardbibliothek (ftplib, urllib, hashlib, +# json, subprocess ...) -> keine externen Pakete noetig. +# +# analyze.py: +# paramiko fuer den SFTP-Download der IONOS-Access-Logs. +paramiko