Compare commits

...
3 Commits
Author SHA1 Message Date
Michael Stangl f74614672b @
Ferienwohnungs-Ausstattung ueberarbeitet

Ausstattungsangaben der FeWos auf ein gemeinsames Feld
gemeinsame_ausstattung umgestellt und die Detail-/Listen-/Startseiten-
Templates (ferienwohnungen/single.html, _default/list.html,
index.html) entsprechend angepasst.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-08-28 16:45:20 +02:00
Michael Stangl eeffa578cb @
Blog- und Veranstaltungsbereich hinzugefuegt

Neuer Bereich /blog fuer Veranstaltungstipps und Neuigkeiten aus
Kaufbeuren: content/blog/ (inkl. erstem Beitrag "Veranstaltungen
Herbst 2026" als Page-Bundle mit Bildern), Layouts layouts/blog/
(list + single) und ein bild-Shortcode, der Bilder aus dem Bundle
aufloest und eine Bildquelle anzeigt. Menueintrag "Blog" in hugo.toml,
zugehoeriges Blog-CSS (und Belegungsplan-Band) in style.css.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-08-28 16:45:09 +02:00
Michael Stangl 2379f4ac9f @
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) <noreply@anthropic.com>
@
2026-08-28 16:44:56 +02:00
46 changed files with 1119 additions and 199 deletions
-16
View File
@@ -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=
+7
View File
@@ -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/
+34 -6
View File
@@ -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`).
+38
View File
@@ -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
+20
View File
@@ -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
+26
View File
@@ -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%
+26
View File
@@ -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%
+29
View File
@@ -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%
+7
View File
@@ -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%"
+54
View File
@@ -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 ================================================================
+27
View File
@@ -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
+77
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

+7
View File
@@ -0,0 +1,7 @@
---
title: "Blog & Veranstaltungen"
description: "Aktuelle Veranstaltungen, Tipps und Neuigkeiten aus Kaufbeuren und dem Allgäu"
---
Hier finden Sie aktuelle Veranstaltungstipps und Neuigkeiten aus Kaufbeuren und
der Region – ideal, um Ihren Aufenthalt bei uns zu planen.
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

@@ -0,0 +1,113 @@
---
title: "Veranstaltungshighlights in Kaufbeuren – Herbst 2026"
date: 2026-08-28
# Einblenddatum: 2 Monate vor dem ersten Event (11.09.2026) -> ab hier sichtbar
publishDate: 2026-07-11
# Ablaufdatum: 1 Monat nach dem ersten Event -> danach ausgeblendet
expiryDate: 2026-10-11
description: "Von Filmsoiree über Genusstage bis zum Jazzkonzert – die schönsten Veranstaltungen rund um Kaufbeuren im Herbst 2026."
bild: "filmsoiree-kino.jpg"
bild_alt: "Kinosaal mit roten Sesseln"
bild_quelle: 'Foto: <a href="https://unsplash.com/@krields" target="_blank" rel="noopener">Krists Luhaers</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>'
---
Der Herbst hält in Kaufbeuren und im Ostallgäu ein buntes Programm bereit. Ob
Kultur, Kulinarik oder ein stimmungsvoller Bummel durch die Altstadt – hier ist
für jeden etwas dabei. Während Ihres Aufenthalts bei uns in Kaufbeuren-Hirschzell
erreichen Sie alle Veranstaltungsorte bequem in wenigen Minuten.
## Filmsoiree: „Zwischen zwei Heimaten"
{{< bild
src="filmsoiree-kino.jpg"
alt="Kinosaal mit roten Sesseln"
quelle=`Foto: <a href="https://unsplash.com/@krields" target="_blank" rel="noopener">Krists Luhaers</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**11. September 2026 · Gablonzer Haus (Großer Saal), Kaufbeuren-Neugablonz**
Anlässlich von „80 Jahre Neugablonz – 80 Jahre Vertreibung" zeigt das
Isergebirgs-Museum in Zusammenarbeit mit dem Kulturreferat eine besondere
Filmsoiree. Der Eintritt ist frei (Zählkarte vorab erforderlich).
## Geführte Pilgerwanderung: Etappe Mindelheim nach Kaufbeuren
{{< bild
src="pilgerwanderung-natur.jpg"
alt="Wanderweg durch grüne Voralpenlandschaft"
quelle=`Foto: <a href="https://unsplash.com/@fab_fotografie" target="_blank" rel="noopener">Fabian Kühne</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**19. September 2026 · Start in Mindelheim, Ziel Crescentiakloster Kaufbeuren**
Eine begleitete Pilgerwanderung auf der rund 28 Kilometer langen 4. Etappe des
Crescentia-Pilgerwegs mit Impulsen und geistlicher Begleitung durch
Pilgerwegbegleiter.
## Allgäuer Genusstage 2026
{{< bild
src="genusstage-kulinarik.jpg"
alt="Regionale Spezialitäten auf einem Tisch"
quelle=`Foto: <a href="https://unsplash.com/@brookelark" target="_blank" rel="noopener">Brooke Lark</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**19. September – 04. Oktober 2026 · Kaufbeuren, Ostallgäu & Region**
Kulinarische Aktionswochen im Landkreis Ostallgäu und der Stadt Kaufbeuren.
Regionale Erzeuger, Gastronomen und Direktvermarkter präsentieren Spezialitäten
aus der Heimat.
## Jubiläumskonzert: 60 Jahre Kulturring Kaufbeuren
{{< bild
src="konzert-orchester.jpg"
alt="Orchester bei einem Konzert"
quelle=`Foto: <a href="https://unsplash.com/@manuelsardo" target="_blank" rel="noopener">Manuel Nägeli</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**26. September 2026 · Stadtsaal Kaufbeuren**
Der Kulturring Kaufbeuren feiert sein 60-jähriges Bestehen mit einer sinfonischen
Märchenstunde, gespielt vom renommierten ODEON Jugendsymphonieorchester München.
## Theater & Performance: „Poseidon und die Plastiksee"
{{< bild
src="theater-buehne.jpg"
alt="Theaterbühne mit Vorhang und Beleuchtung"
quelle=`Foto: <a href="https://unsplash.com/@barnaba" target="_blank" rel="noopener">Barna Bartis</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**07. Oktober 2026 · Stadttheater Kaufbeuren**
Ein Gastspiel der Saison des Kulturrings Kaufbeuren e.V., das sich auf
künstlerische Weise mit Umwelt- und Meeres-Themen im historischen Rahmen des
Stadttheaters auseinandersetzt.
## Jazzkonzert: Yakou Tribe – „Out of Sight"
{{< bild
src="jazz-konzert.jpg"
alt="Musiker bei einem Jazzkonzert"
quelle=`Foto: <a href="https://unsplash.com/@mariusmasalar" target="_blank" rel="noopener">Marius Masalar</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**30. Oktober 2026 · Stadttheater Kaufbeuren**
Das renommierte Berliner Jazz-Ensemble Yakou Tribe gastiert im Stadttheater
Kaufbeuren und präsentiert modernen, mitreißenden Instrumental-Jazz.
## Altstadtfunkeln (Candle Light Shopping & Kaufbeuren Leuchtet)
{{< bild
src="altstadtfunkeln-nacht.jpg"
alt="Illuminierte Altstadtgasse bei Nacht"
quelle=`Foto: <a href="https://unsplash.com/@agk42" target="_blank" rel="noopener">Alex Knight</a> auf <a href="https://unsplash.com" target="_blank" rel="noopener">Unsplash</a>` >}}
**13. und 14. November 2026 · Altstadt Kaufbeuren**
Die Zusammenführung von „Candle Light Shopping" und „Kaufbeuren leuchtet".
Besucher erwarten stimmungsvolle Videomappings an historischen Fassaden,
illuminierte Gassen und verlängerte Einkaufszeiten im lokalen Einzelhandel.
---
*Die verwendeten Fotos sind Symbolbilder (Unsplash-Lizenz, kostenlos auch für
kommerzielle Nutzung). Alle Veranstaltungsangaben ohne Gewähr – bitte informieren
Sie sich vor Ihrem Besuch bei den jeweiligen Veranstaltern über eventuelle
Änderungen.*
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+58 -44
View File
@@ -1,32 +1,38 @@
---
title: "Unsere Ferienwohnungen"
description: "Drei komfortable Ferienwohnungen in Kaufbeuren-Hirschzell"
allgemein:
- titel: "Sicherheit"
gemeinsame_ausstattung:
- titel: "Kochen und Essen"
punkte:
- "Rauchmelder"
- "Verbandskasten"
- titel: "Eigenschaften"
- "Backofen"
- "Dunstabzugshaube"
- "Elektroherd"
- "Esstisch/Essecke"
- "Geschirr"
- "Geschirrhandtücher"
- "Kühlschrank"
- "Kaffeemaschine Filter"
- "Mikrowelle"
- "Mixer"
- "Toaster"
- "Wasserkocher"
- titel: "Haushaltshilfen"
punkte:
- "Seniorengerecht"
- "Nichtraucher"
- "Allergikergeeignet"
- "bedingt barrierefrei"
- "Haustiere nicht erlaubt"
- titel: "Für die Kleinen"
- "Bügelbrett"
- "Bügeleisen"
- "Putzmittel"
- "Staubsauger"
- "Wäscheständer"
- "Wäschetrockner"
- "Wäschewaschen möglich"
- titel: "Wohnen und Leben"
punkte:
- "Kinderhochstuhl"
- "Wickelkommode"
- "Wickelauflage"
- "Babybett (Reisebett)"
- "Kindertritt"
- "Baby-Badewanne"
umfeld:
- titel: "Im Grünen"
punkte:
- "Terrasse"
- "Gartenmöbel"
- "Sonnenschirm"
- "Flachbildfernseher"
- "Fliegenschutzgitter"
- "Heizung/Zentralheizung"
- "Internet WLAN"
- "Kabel-TV"
- "Radio"
- titel: "Entfernungen"
punkte:
- "PKW-Stellplatz: 10 m"
@@ -41,35 +47,43 @@ umfeld:
- "Apotheke: 3,0 km"
- "Autobahn: 20 km"
- "Flughafen: 45 km"
- titel: "Spiel, Sport, Spaß"
- titel: "Eigenschaften"
punkte:
- "Tischtennis"
- "Spielplatz"
gemeinsam:
- titel: "Wohnen und Leben"
- "Allergikergeeignet"
- "bedingt barrierefrei"
- "Haustiere nicht erlaubt"
- "Nichtraucher"
- "Seniorengerecht"
- titel: "Für die Kleinen"
punkte:
- "Heizung/Zentralheizung"
- "Fliegenschutzgitter"
- "Flachbildfernseher"
- "Kabel-TV"
- "Radio"
- "Internet WLAN"
- "Baby-Badewanne"
- "Babybett (Reisebett)"
- "Kinderhochstuhl"
- "Kindertritt"
- titel: "Bad und Sanitär"
punkte:
- "Dusche"
- "Waschbecken"
- "Spiegel"
- "Haartrockner"
- "Spiegel"
- "Waschbecken"
- "WC"
- titel: "Haushaltshilfen"
- titel: "Spiel, Sport, Spaß"
punkte:
- "Bügelbrett"
- "Bügeleisen"
- "Staubsauger"
- "Wäschewaschen möglich"
- "Wäscheständer"
- "Wäschetrockner"
- "Putzmittel"
- "Spielplatz"
- "Tischtennis"
- titel: "Im Grünen"
punkte:
- "Gartenmöbel"
- "Sonnenschirm"
- "Terrasse"
- titel: "Sicherheit"
punkte:
- "Rauchmelder"
- "Verbandskasten"
- titel: "Fuhrpark"
punkte:
- "PKW-Stellplatz"
---
Alle drei Wohnungen sind vollständig ausgestattet, allergikerfreundlich und familientauglich.
+3 -12
View File
@@ -16,20 +16,11 @@ ausstattung:
- "1 Einzelbett (90×200 cm)"
- titel: "Kochen und Essen"
punkte:
- "Elektroherd"
- "Backofen"
- "Dunstabzugshaube"
- "Geschirrspüler"
- "Kühlschrank"
- "Gefriertruhe"
- "Kaffeemaschine (Filter)"
- "Wasserkocher"
- "Toaster"
- "Mixer"
- "Mikrowelle"
- "Esstisch/Essecke"
- "Geschirr"
- "Geschirrhandtücher"
- titel: "Für die Kleinen"
punkte:
- "Wickelkommode"
bilder:
- bild: "/images/fewo1/ferienwohnung-1-wolframm-kaufbeuren-Wohnzimmer.jpg"
titel: "Ferienwohnung Kaufbeuren Wohnzimmer FeWo 1"
+3 -12
View File
@@ -14,19 +14,10 @@ ausstattung:
- "1 Zustellbett (80×190 cm)"
- titel: "Kochen und Essen"
punkte:
- "Elektroherd"
- "Backofen"
- "Dunstabzugshaube"
- "Mikrowelle"
- "Kühlschrank"
- "Gefrierfach"
- "Kaffeemaschine (Filter)"
- "Wasserkocher"
- "Toaster"
- "Mixer"
- "Esstisch/Essecke"
- "Geschirr"
- "Geschirrhandtücher"
- titel: "Für die Kleinen"
punkte:
- "Wickelunterlage"
bilder:
- bild: "/images/fewo2/Wohn-esszimmer-Front-fewo-2.jpg"
titel: "Ferienwohnung Kaufbeuren Wohn-Esszimmer FeWo 2"
+2 -14
View File
@@ -15,22 +15,10 @@ ausstattung:
- "1 Zustellbett (80×190 cm)"
- titel: "Kochen und Essen"
punkte:
- "Elektroherd"
- "Backofen"
- "Dunstabzugshaube"
- "Kühlschrank"
- "Gefrierfach"
- "Kaffeemaschine (Filter)"
- "Wasserkocher"
- "Toaster"
- "Mixer"
- "Mikrowelle"
- "Esstisch/Essecke"
- "Geschirr"
- "Geschirrhandtücher"
- titel: "Besonderheit"
- titel: "Für die Kleinen"
punkte:
- "Kinderfreundlich"
- "Wickelkommode"
bilder:
- bild: "/images/fewo3/ferienwohnung-allgaeu-kaufbeuren-wohnung-3-Kueche-Sitzecke-fewo.jpg"
titel: "Ferienwohnung Kaufbeuren FeWo 3 – Sitzecke in der Küche"
-10
View File
@@ -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
-10
View File
@@ -1,10 +0,0 @@
@echo off
echo.
echo === IWZ Ferienwohnungen - Deploy ===
echo.
python deploy.py
if %errorlevel% neq 0 (
pause
exit /b 1
)
pause
+146
View File
@@ -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 <https://goaccess.io/download>, 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=<z.B. home123456789.1and1-data.host oder access-5000000000.webspace-host.com>
IONOS_SFTP_PORT=22
IONOS_SFTP_USER=<SFTP-Benutzer>
IONOS_SFTP_PASSWORD=<SFTP-Passwort>
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 <https://goaccess.io/man#custom-log>).
**`paramiko`-Authentifizierungsfehler.**
SFTP-Host/-User/-Passwort im IONOS-Kundenbereich gegenprüfen. Der SFTP-Benutzer
ist nicht zwingend identisch mit dem Login des Kundenbereichs.
+5 -1
View File
@@ -28,7 +28,11 @@ defaultContentLanguage = "de"
name = "Ausflugsziele"
url = "/ausflugsziele/"
weight = 4
[[menu.main]]
name = "Blog"
url = "/blog/"
weight = 5
[[menu.main]]
name = "Kontakt"
url = "/kontakt/"
weight = 5
weight = 6
+11 -49
View File
@@ -30,57 +30,19 @@
{{ end }}
</div>
{{ if or .Params.allgemein .Params.umfeld .Params.gemeinsam }}
{{ with .Params.gemeinsame_ausstattung }}
<div class="umfeld" id="umfeld">
<p class="umfeld-intro">Diese Ausstattung und Umgebungsinfos gelten für alle drei Ferienwohnungen gleichermaßen.</p>
{{ with .Params.allgemein }}
<section class="umfeld-gruppe">
<h2>Allgemein</h2>
<div class="spec-grid">
{{ range . }}
<div class="spec-block">
<h3>{{ .titel }}</h3>
<ul>
{{ range .punkte }}<li>{{ . }}</li>{{ end }}
</ul>
</div>
{{ end }}
<p class="umfeld-intro">Diese Ausstattung gilt für alle drei Ferienwohnungen gleichermaßen.</p>
<div class="spec-grid">
{{ range . }}
<div class="spec-block">
<h3>{{ .titel }}</h3>
<ul>
{{ range .punkte }}<li>{{ . }}</li>{{ end }}
</ul>
</div>
</section>
{{ end }}
{{ with .Params.umfeld }}
<section class="umfeld-gruppe">
<h2>Umfeld</h2>
<div class="spec-grid">
{{ range . }}
<div class="spec-block">
<h3>{{ .titel }}</h3>
<ul>
{{ range .punkte }}<li>{{ . }}</li>{{ end }}
</ul>
</div>
{{ end }}
</div>
</section>
{{ end }}
{{ with .Params.gemeinsam }}
<section class="umfeld-gruppe">
<h2>Gemeinsame Ausstattung</h2>
<div class="spec-grid">
{{ range . }}
<div class="spec-block">
<h3>{{ .titel }}</h3>
<ul>
{{ range .punkte }}<li>{{ . }}</li>{{ end }}
</ul>
</div>
{{ end }}
</div>
</section>
{{ end }}
{{ end }}
</div>
</div>
{{ end }}
</div>
+33
View File
@@ -0,0 +1,33 @@
{{ define "main" }}
<article class="page">
<div class="container">
<h1>{{ .Title }}</h1>
{{ .Content }}
<div class="fewo-grid blog-grid">
{{ range .Pages.ByPublishDate.Reverse }}
<div class="fewo-card">
{{ $post := . }}
{{ with .Params.bild }}
{{ $src := . }}
{{ with $post.Resources.GetMatch $src }}{{ $src = .RelPermalink }}{{ end }}
<a href="{{ $post.RelPermalink }}" class="fewo-card-imglink">
<img src="{{ $src }}" alt="{{ with $post.Params.bild_alt }}{{ . }}{{ else }}{{ $post.Title }}{{ end }}" class="fewo-card-img" loading="lazy">
</a>
{{ end }}
<div class="fewo-card-body">
<p class="blog-date">{{ .PublishDate.Format "02.01.2006" }}</p>
<h2><a href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
{{ with .Params.description }}<p>{{ . }}</p>{{ end }}
<a href="{{ .RelPermalink }}" class="btn btn-primary">Weiterlesen</a>
</div>
</div>
{{ end }}
</div>
{{ if not .Pages }}
<p>Aktuell sind keine Beiträge veröffentlicht. Schauen Sie bald wieder vorbei!</p>
{{ end }}
</div>
</article>
{{ end }}
+18
View File
@@ -0,0 +1,18 @@
{{ define "main" }}
<article class="page blog-post">
<div class="container container--narrow">
<p class="blog-date">{{ .PublishDate.Format "02.01.2006" }}</p>
<h1>{{ .Title }}</h1>
{{ with .Params.bild }}
{{ $src := . }}
{{ with $.Resources.GetMatch $src }}{{ $src = .RelPermalink }}{{ end }}
<figure class="blog-hero">
<img src="{{ $src }}" alt="{{ with $.Params.bild_alt }}{{ . }}{{ else }}{{ $.Title }}{{ end }}" class="blog-hero-img">
{{ with $.Params.bild_quelle }}<figcaption class="bild-quelle">Bildquelle: {{ . | safeHTML }}</figcaption>{{ end }}
</figure>
{{ end }}
{{ .Content }}
<p class="blog-back"><a href="/blog/" class="btn btn-outline">&larr; Zurück zur Übersicht</a></p>
</div>
</article>
{{ end }}
+18 -2
View File
@@ -41,9 +41,26 @@
{{ .Content }}
</div>
{{ $gemeinsam := (.Site.GetPage "/ferienwohnungen").Params.gemeinsame_ausstattung }}
{{ with .Params.ausstattung }}
<section class="ausstattung">
<h2>Ausstattung</h2>
<h2>Ausstattung spezifisch</h2>
<div class="spec-grid">
{{ range . }}
<div class="spec-block">
<h3>{{ .titel }}</h3>
<ul>
{{ range .punkte }}<li>{{ . }}</li>{{ end }}
</ul>
</div>
{{ end }}
</div>
</section>
{{ end }}
{{ with $gemeinsam }}
<section class="ausstattung">
<h2>Ausstattung allgemein</h2>
<div class="spec-grid">
{{ range . }}
<div class="spec-block">
@@ -54,7 +71,6 @@
</div>
{{ end }}
</div>
<p class="ausstattung-hinweis">Allgemeine Ausstattung (Sicherheit, Eigenschaften, für die Kleinen), Umfeld (Garten, Freizeit, Entfernungen) und weitere gemeinsame Ausstattung finden Sie unter <a href="/ferienwohnungen/#umfeld">Allgemein, Umfeld &amp; gemeinsame Ausstattung</a> aller Ferienwohnungen.</p>
</section>
{{ end }}
+15 -1
View File
@@ -17,6 +17,15 @@
</div>
</section>
<section class="belegungsplan-band">
<div class="container">
<button type="button" class="btn belegungsplan-band-btn" id="belegungsplan-btn" aria-expanded="false" aria-controls="belegungsplan-frame">
Belegungsplan – hier freie Kapazitäten einsehen
</button>
<div class="belegungsplan-frame" id="belegungsplan-frame" data-src="https://www.webplanner.de/tools/?76703072363837353064343069" hidden></div>
</div>
</section>
{{/* Highlights-Block vorerst ausgeblendet – siehe Wunsch, dass "Unsere Wohnungen" zuerst steht.
<section class="highlights">
<div class="container">
@@ -70,7 +79,12 @@
{{ with $page.Params.personen }}<span>bis {{ . }} Personen</span>{{ end }}
{{ with $page.Params.preis }}<span>ab {{ . }} € / Nacht</span>{{ end }}
</p>
<a href="{{ $page.RelPermalink }}" class="btn btn-primary">Details &amp; Buchung</a>
<div class="fewo-card-actions">
<a href="{{ $page.RelPermalink }}" class="btn btn-outline">Details</a>
{{ with $page.Params.belegungsplan }}
<a href="{{ . }}" class="btn btn-primary" target="_blank" rel="noopener">Buchung</a>
{{ end }}
</div>
</div>
</div>
{{ end }}
+11
View File
@@ -0,0 +1,11 @@
{{- $src := .Get "src" -}}
{{- $alt := .Get "alt" -}}
{{- $quelle := .Get "quelle" -}}
{{- /* Bild als Page-Resource aus dem Bundle auflösen; sonst src unverändert nutzen */ -}}
{{- with .Page.Resources.GetMatch $src }}{{ $src = .RelPermalink }}{{ end -}}
<figure class="blog-figure">
<img src="{{ $src }}" alt="{{ $alt }}" loading="lazy">
{{- with $quelle }}
<figcaption class="bild-quelle">Bildquelle: {{ . | safeHTML }}</figcaption>
{{- end }}
</figure>
+226
View File
@@ -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())
+11 -3
View File
@@ -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()
+11 -3
View File
@@ -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()
+12 -3
View File
@@ -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)
-8
View File
@@ -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
+9
View File
@@ -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
+62 -5
View File
@@ -257,9 +257,34 @@ img { max-width: 100%; height: auto; display: block; }
.highlight-card h3 { margin-bottom: 0.5rem; font-size: 1.05rem; }
.highlight-card p { color: var(--text-muted); font-size: 0.9rem; }
/* ===== Belegungsplan-Band (auffälliger Verweis auf der Startseite) ===== */
.belegungsplan-band {
background: #c0392b;
padding: 1.6rem 0;
text-align: center;
}
.belegungsplan-band-btn {
background: #e67e22;
color: #fff;
border: none;
font-size: 1.15rem;
font-weight: 700;
padding: 0.9rem 2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
}
.belegungsplan-band-btn:hover {
background: #d35400;
color: #fff;
}
/* ===== Belegungsplan (Übersicht ein-/ausblenden) ===== */
.belegungsplan-toggle { margin: 1rem 0 2.5rem; }
.belegungsplan-frame { margin-top: 1.5rem; }
.belegungsplan-frame {
margin-top: 1.5rem;
background: #fff;
padding: 1rem;
border-radius: var(--radius);
}
.belegungsplan-frame[hidden] { display: none; }
/* ===== FeWo Grid ===== */
@@ -281,6 +306,8 @@ img { max-width: 100%; height: auto; display: block; }
.fewo-card-img { width: 100%; height: 200px; object-fit: cover; transition: opacity 0.2s; }
.fewo-card-imglink:hover .fewo-card-img { opacity: 0.9; }
.fewo-card-body { padding: 1.5rem; }
.fewo-card-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.fewo-card-actions .btn { margin-right: 0; }
.fewo-card-body h2, .fewo-card-body h3 { margin-bottom: 0.5rem; font-size: 1.2rem; }
.fewo-card-body h2 a, .fewo-card-body h3 a { color: var(--text); }
.fewo-meta {
@@ -311,10 +338,13 @@ img { max-width: 100%; height: auto; display: block; }
/* ===== Karten-Grid (wiederverwendet für Umfeld + Ausstattung) ===== */
.spec-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1.25rem;
}
@media (max-width: 760px) {
@media (max-width: 980px) {
.spec-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
.spec-grid { grid-template-columns: 1fr; }
}
.spec-block {
@@ -337,7 +367,6 @@ img { max-width: 100%; height: auto; display: block; }
/* ===== Ausstattung (einzelne Ferienwohnung) ===== */
.ausstattung { margin-top: 2rem; }
.ausstattung h2 { margin: 1.5rem 0 1rem; font-size: 1.2rem; border-bottom: 1px solid var(--border); padding-bottom: 0.25rem; }
.ausstattung-hinweis { margin-top: 1.5rem; font-size: 0.95rem; color: var(--text-muted); }
/* ===== Ausflugsziele-Gruppen ===== */
.ausflug-gruppe { margin-top: 3rem; }
@@ -548,7 +577,7 @@ img { max-width: 100%; height: auto; display: block; }
content: "";
position: absolute;
inset: 0;
background: rgba(255,255,255,0.6);
background: transparent;
}
.zitat-box-inner {
position: relative;
@@ -610,3 +639,31 @@ img { max-width: 100%; height: auto; display: block; }
/* ===== Main ===== */
.main { min-height: calc(100vh - var(--nav-height) - 200px); }
/* ===== Blog ===== */
.blog-grid { margin-top: 2rem; }
.blog-date { color: var(--muted, #777); font-size: 0.85rem; margin-bottom: 0.4rem; }
.container--narrow { max-width: 760px; }
.blog-hero-img {
width: 100%;
max-height: 420px;
object-fit: cover;
border-radius: var(--radius, 6px);
}
.blog-post h2 { margin-top: 2.5rem; }
.blog-back { margin-top: 2.5rem; }
.blog-figure { margin: 1rem 0 1.5rem; }
.blog-figure img {
width: 100%;
max-height: 380px;
object-fit: cover;
border-radius: var(--radius, 6px);
}
.blog-hero { margin: 0 0 2rem; }
.bild-quelle {
font-size: 0.78rem;
color: var(--muted, #777);
margin-top: 0.4rem;
line-height: 1.4;
}
.bild-quelle a { color: var(--muted, #777); text-decoration: underline; }