11 Commits

22 changed files with 1106 additions and 187 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
.git
.venv
__pycache__
*.pyc
*.pyo
*.pyd
*.log
build/
*.ics
*.pdf

View File

@@ -0,0 +1,65 @@
name: Build Standalone Releases
on:
workflow_dispatch:
push:
tags:
- "v*"
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: linux
runs-on: ubuntu-latest
shell: bash
build_cmd: |
chmod +x build/build_linux.sh build/package_linux.sh
./build/build_linux.sh
./build/package_linux.sh
artifact_glob: release/PDFtoICS-linux-v*.tar.gz
- os: macos
runs-on: macos-latest
shell: bash
build_cmd: |
chmod +x build/build_macos.sh build/package_macos.sh
./build/build_macos.sh
./build/package_macos.sh
artifact_glob: release/PDFtoICS-macos-v*.zip
- os: windows
runs-on: windows-latest
shell: pwsh
build_cmd: |
cmd /c build\build_windows.cmd
cmd /c build\package_windows.cmd
artifact_glob: release/PDFtoICS-windows-v*.zip
runs-on: ${{ matrix.runs-on }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Create venv
shell: ${{ matrix.shell }}
run: |
python -m venv .venv
- name: Build + Package
shell: ${{ matrix.shell }}
run: ${{ matrix.build_cmd }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-release
path: ${{ matrix.artifact_glob }}

6
.gitignore vendored
View File

@@ -3,3 +3,9 @@
.venv/ .venv/
__pycache__/ __pycache__/
.pdf_to_ics_config.json .pdf_to_ics_config.json
# Build artifacts
dist/
release/
*.spec
build/PDFtoICS/

152
BUILD_STANDALONE.md Normal file
View File

@@ -0,0 +1,152 @@
# Standalone Builds (Linux, macOS, Windows)
Diese Anleitung erstellt eigenständige Anwendungen mit **PyInstaller** auf dem jeweiligen Zielbetriebssystem.
## Wichtig
- Builds müssen **nativ pro OS** erstellt werden (kein Cross-Compile mit diesen Skripten).
- Verwenden Sie eine aktive und funktionierende `.venv` im Projektordner.
- Die GUI wird aus `gui_wxpython.py` gebaut.
---
## Release auf einen Blick
### Linux
```bash
./build/build_linux.sh
./build/package_linux.sh
```
### macOS
```bash
./build/build_macos.sh
./build/package_macos.sh
```
### Windows
```cmd
build\build_windows.cmd
build\package_windows.cmd
```
---
## Empfohlene Release-Reihenfolge
1. Version erhöhen (z. B. `version.txt` und Changelog)
2. Pro Zielplattform Build + Packaging ausführen
3. Artefakte im `release/`-Ordner prüfen
4. Git-Commit erstellen und Tag setzen (z. B. `v1.2.0`)
5. Tag und Branch pushen
6. Release-Artefakte auf der Release-Seite hochladen
Beispiel Git-Workflow:
```bash
git add -A
git commit -m "Release x.y.z"
git tag -a vx.y.z -m "vx.y.z"
git push origin main
git push origin vx.y.z
```
Optional per CI:
- Workflow: `.gitea/workflows/build-standalone-release.yml`
- Trigger: Tag-Push `v*`
- Ergebnis: Plattform-Artefakte als CI-Artefakte
---
## Linux
```bash
chmod +x build/build_linux.sh
./build/build_linux.sh
```
Ergebnis:
- `dist/PDFtoICS/` (Ordner mit ausführbarer Datei)
Optional als Release-Archiv (`.tar.gz`) verpacken:
```bash
chmod +x build/package_linux.sh
./build/package_linux.sh
```
Ergebnis:
- `release/PDFtoICS-linux-v<VERSION>.tar.gz`
---
## macOS
```bash
chmod +x build/build_macos.sh
./build/build_macos.sh
```
Ergebnis:
- `dist/PDFtoICS.app`
Hinweis:
- Für öffentliche Verteilung ist Code-Signing/Notarisierung empfohlen.
Optional als Release-Archiv (`.zip`) verpacken:
```bash
chmod +x build/package_macos.sh
./build/package_macos.sh
```
Ergebnis:
- `release/PDFtoICS-macos-v<VERSION>.zip`
---
## Windows
Starten Sie unter Windows:
```cmd
build\build_windows.cmd
```
Ergebnis:
- `dist\PDFtoICS\PDFtoICS.exe`
Hinweis:
- Für weniger SmartScreen-Warnungen ist Signierung empfohlen.
Optional als Release-Archiv (`.zip`) verpacken:
```cmd
build\package_windows.cmd
```
Ergebnis:
- `release\PDFtoICS-windows-v<VERSION>.zip`
---
## Clean Build
PyInstaller erstellt `build/` und `dist/` sowie eine `.spec` Datei im Projektverzeichnis.
Optionales Aufräumen:
```bash
rm -rf build dist *.spec
```
Unter Windows:
```cmd
rmdir /s /q build
rmdir /s /q dist
del /q *.spec
```

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY web/requirements-web.txt /app/web/requirements-web.txt
RUN pip install --no-cache-dir -r /app/web/requirements-web.txt
COPY . /app
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "web.app:app", "--host", "0.0.0.0", "--port", "8000"]

281
README.md
View File

@@ -1,153 +1,129 @@
# PDF zu ICS Konverter - Dienstplan Importer # PDF zu ICS Konverter - Dienstplan Importer
Dieses Tool extrahiert Kalenderdaten aus Dienstplan-PDFs und konvertiert sie in das iCalendar-Format (ICS), das von den meisten Kalenderanwendungen importiert werden kann. Dieses Tool extrahiert Kalenderdaten aus Dienstplan-PDFs und konvertiert sie in das iCalendar-Format (ICS), das in gängigen Kalender-Apps importiert werden kann.
## 🎯 Zwei Versionen verfügbar ## 🎯 Varianten
### 1. **GUI-Version** (Grafische Oberfläche) - Empfohlen! ### 1) GUI (empfohlen)
Benutzerfreundliche grafische Oberfläche mit Drag & Drop Support. Native Oberfläche mit wxPython:
```bash ```bash
./start_gui.sh ./start_gui.sh
``` ```
**Features:** **Highlights:**
- Drag & Drop für PDF-Dateien - Drag & Drop für PDFs
- 📋 Mehrere PDFs gleichzeitig verarbeiten - Mehrere PDFs gleichzeitig
- 📁 Ausgabe-Verzeichnis frei wählbar - Frei wählbares Ausgabe-Verzeichnis
- 📊 Live-Log und Fortschrittsanzeige - Live-Log mit Fortschritt
- 💾 Merkt sich letzte Verzeichnisse - Optionale Filter:
- Ruhetage ausschließen
- Urlaub (060/0060) ausschließen
**Voraussetzung:** wxPython muss installiert sein (wird automatisch versucht; siehe [WXPYTHON_README.md](WXPYTHON_README.md)) ### 2) CLI
Interaktives Textmenü:
### 2. **CLI-Version** (Kommandozeile)
Textbasiertes Menü für die Kommandozeile.
```bash ```bash
./start.sh ./start.sh
``` ```
**Features:** ### 3) Web (MVP für Mobilgeräte)
- ⚡ Funktioniert überall ohne zusätzliche Abhängigkeiten Browser-Variante mit Upload + direktem ICS-Download:
- 🔄 Automatische Verarbeitung aller PDFs im Verzeichnis
- 📝 Textbasiertes interaktives Menü
## Features ```bash
./start_web.sh
✅ Extrahiert Dienstplan-Informationen aus PDFs
✅ Erkennt Schicht-Zeitangaben (z.B. 04:51-15:46)
✅ Handhabt Nachtschichten korrekt (über Mitternacht hinaus)
✅ Erstellt Standard-konforme ICS-Dateien
✅ Unterstützt mehrere PDFs gleichzeitig
✅ GUI mit Drag & Drop (optional)
✅ CLI-Menü für schnelle Nutzung
## Installation
### Windows (Empfohlen)
1. Installieren Sie **Python 3.10+** und aktivieren Sie bei der Installation die Option **"Add Python to PATH"**.
2. Öffnen Sie den Projektordner.
3. Starten Sie die GUI per Doppelklick auf `start_gui.cmd`.
Beim ersten Start wird automatisch eine virtuelle Umgebung (`.venv`) erstellt und alle benötigten Pakete installiert.
**Alternative (CLI):** Doppelklick auf `start.cmd`.
**Falls Python nicht gefunden wird (CMD im Projektordner):**
```bat
py -3 -m venv .venv --upgrade-deps
.\.venv\Scripts\python.exe gui_wxpython.py
``` ```
### Schnellstart (Empfohlen) Details siehe [WEB_README.md](WEB_README.md).
Docker-Variante (Server):
```bash
docker compose up -d --build
```
---
## ⚡ Schnellstart (60 Sekunden)
1. GUI starten (`./start_gui.sh` oder `start_gui.cmd` unter Windows)
2. PDF-Dateien hinzufügen
3. Optional Filter aktivieren
4. Auf **"ICS Datei erstellen"** klicken
---
## 🧩 Installation nach Betriebssystem
### Linux (Ubuntu/Debian/Mint)
**Für GUI-Version:**
```bash ```bash
./start_gui.sh ./start_gui.sh
``` ```
Siehe [WXPYTHON_README.md](WXPYTHON_README.md) für wxPython-Hinweise.
**Für CLI-Version:** Beim ersten Start wird automatisch `.venv` erstellt und alles installiert.
Wenn `wxPython` nicht installiert werden kann, helfen häufig:
```bash ```bash
./start.sh sudo apt-get update
sudo apt-get install -y build-essential python3-dev libgtk-3-dev libglib2.0-dev libjpeg-dev libtiff-dev libpng-dev
``` ```
### GUI-Version (Empfohlen) Für systemweite Installation mit Menüeintrag siehe [INSTALL.md](INSTALL.md).
### macOS
1. Starten Sie die GUI:
```bash ```bash
./start_gui.sh ./start_gui.sh
``` ```
2. Fügen Sie PDF-Dateien hinzu (Button oder Drag & Drop).
3. Optional: Aktivieren Sie Filter wie „Ruhetage ausschließen“ und „Urlaub ausschließen (060, 0060)“.
4. Klicken Sie auf „📄 ICS Datei erstellen“.
Die GUI merkt sich Ihre letzten Verzeichnisse und Exportoptionen. Beim ersten Start wird `.venv` erstellt und die Abhängigkeiten werden installiert.
### CLI-Version ### Windows
Starten Sie das interaktive Menü: 1. Python 3.10+ installieren (Option **Add Python to PATH** aktivieren)
2. `start_gui.cmd` per Doppelklick starten
```bash Beim ersten Start wird `.venv` automatisch eingerichtet.
./start.sh
```
Beide Skripte erstellen automatisch eine Python Virtual Environment und installieren alle benötigten Abhängigkeiten. ---
### Manuelle Installation ## 🧱 Standalone-Apps (ohne Python beim Endnutzer)
Für Build/Packaging auf Linux, macOS und Windows:
Die erforderlichen Dependencies sind bereits installiert. Falls Sie das Projekt neu einrichten: - [BUILD_STANDALONE.md](BUILD_STANDALONE.md)
```bash Dort enthalten:
python3 -m venv .venv - Build-Skripte pro OS
source .venv/bin/activate - Packaging-Skripte für Release-Artefakte
pip install pdfplumber icalendar pypdf2 pytz packaging - empfohlene Release-Reihenfolge
```
## Verwendung ---
### Schnellstart (CLI) ## 🖥️ CLI-Nutzung
1. Kopieren Sie Ihre Dienstplan-PDF-Dateien in ein Verzeichnis ### Beispiele
2. Führen Sie das Skript aus:
```bash
python3 pdf_to_ics.py
```
Das Tool findet automatisch alle `.pdf` Dateien im aktuellen Verzeichnis und erstellt entsprechende `.ics` Dateien.
### Kommandozeilen-Optionen
```bash ```bash
# Alle PDFs im aktuellen Verzeichnis konvertieren # Alle PDFs im aktuellen Verzeichnis konvertieren
python3 pdf_to_ics.py python3 pdf_to_ics.py
# PDFs aus einem bestimmten Verzeichnis konvertieren # Input/Output-Verzeichnis setzen
python3 pdf_to_ics.py --input ./pdfs
# PDFs in anderes Verzeichnis speichern
python3 pdf_to_ics.py --input ./pdfs --output ./ics_dateien python3 pdf_to_ics.py --input ./pdfs --output ./ics_dateien
# Ruhetage ausschließen # Ruhetage ausschließen
python3 pdf_to_ics.py --exclude-rest python3 pdf_to_ics.py --exclude-rest
# Urlaub (060) ausschließen # Urlaub (060/0060) ausschließen
python3 pdf_to_ics.py --exclude-vacation python3 pdf_to_ics.py --exclude-vacation
# Einzelne PDF-Datei konvertieren # Einzelne PDF-Datei
python3 pdf_to_ics.py /pfad/zur/datei.pdf python3 pdf_to_ics.py /pfad/zur/datei.pdf
# Mit detaillierter Ausgabe
python3 pdf_to_ics.py --input ./pdfs -v
# Hilfe anzeigen
python3 pdf_to_ics.py --help
``` ```
**Verfügbare Optionen:** ### Optionen
| Option | Kurzform | Beschreibung | | Option | Kurzform | Beschreibung |
|--------|----------|-------------| |--------|----------|-------------|
@@ -155,105 +131,44 @@ python3 pdf_to_ics.py --help
| `--output DIR` | `-o` | Ausgabe-Verzeichnis für ICS-Dateien (Standard: Eingabe-Verzeichnis) | | `--output DIR` | `-o` | Ausgabe-Verzeichnis für ICS-Dateien (Standard: Eingabe-Verzeichnis) |
| `--exclude-rest` | `-e` | Ruhetage ausschließen (Ruhe, R56, R36, vRWF48, RWE, vR48) | | `--exclude-rest` | `-e` | Ruhetage ausschließen (Ruhe, R56, R36, vRWF48, RWE, vR48) |
| `--exclude-vacation` | `-u` | Urlaub ausschließen (060, 0060) | | `--exclude-vacation` | `-u` | Urlaub ausschließen (060, 0060) |
| `--verbose` | `-v` | Detaillierte Ausgabe anzeigen | | `--verbose` | `-v` | Detaillierte Ausgabe |
| `--help` | `-h` | Hilfe anzeigen | | `--help` | `-h` | Hilfe anzeigen |
### Erweiterte Nutzung ---
Sie können auch direkt mit dem Python-Modul arbeiten: ## 📅 ICS-Import
```python Die erzeugten `.ics`-Dateien lassen sich u. a. in folgende Kalender importieren:
from pdf_to_ics import extract_dienstplan_data, create_ics_from_dienstplan - Outlook
- Google Kalender
- Apple Kalender
- Thunderbird
- LibreOffice
- Android-Kalender-Apps
# PDF verarbeiten ---
dienstplan = extract_dienstplan_data('meine_pdf.pdf')
# ICS erstellen ## 🛠️ Fehlerbehebung
create_ics_from_dienstplan(dienstplan, 'mein_kalender.ics')
- **Keine Events gefunden:** PDF-Layout prüfen
- **GUI startet nicht:** `.venv` löschen und neu starten
```bash
rm -rf .venv
./start_gui.sh
``` ```
- **Zeitzone ändern:** in `pdf_to_ics.py` den Wert `Europe/Berlin` anpassen
## Dateiformat
### ICS-Datei importieren
Die erstellte `.ics` Datei kann in folgende Kalenderanwendungen importiert werden:
- **Outlook**: Datei → Öffnen und exportieren → Importieren
- **Google Kalender**: Einstellungen → Kalender importieren
- **iCal/macOS**: Doppelklick auf die .ics Datei
- **Thunderbird**: Kalender → Kalender importieren
- **Android**: Mit einer Kalender-App öffnen
- **LibreOffice**: Datei → Öffnen
## Struktur der extrahierten Daten
Das Tool extrahiert folgende Informationen aus der PDF:
- **Name und Personalnummer**: Des Mitarbeiters
- **Betriebshof**: Standort
- **Sollarbeitszeit**: Gewünschte Arbeitszeit pro Monat
- **Events**: Einzelne Schichten mit:
- Datum
- Dienstart (z.B. "36234", "Ruhe", "Dispo")
- Zeitangabe (falls vorhanden)
## Output
Für jede verarbeitete PDF wird eine entsprechende ICS-Datei erstellt:
```
2026-02-23 DB Köhler00100718_März2026.pdf → 2026-02-23 DB Köhler00100718_März2026.ics
```
Die ICS-Datei enthält ein Event für jeden Arbeitstag mit:
- **Titel**: Name - Dienstart
- **Beschreibung**: Dienstart und Betriebshof
- **Zeit**: Mit aktueller Zeitzone (Europe/Berlin)
## Fehlerbehebung
### Keine Events gefunden?
- Stellen Sie sicher, dass die PDF das erwartete Tabellenformat hat
- Überprüfen Sie die Dateiname und die PDF-Struktur
### Zeitzone falsch?
- Die aktuelle Einstellung ist Europe/Berlin
- Zum Ändern: Bearbeiten Sie die Zeile in `pdf_to_ics.py`:
```python
tz = pytz.timezone('Europe/Berlin') # Ändern SIe diesen Wert
```
## Technische Details
### Projektstruktur
```
ICS-Import/
├── pdf_to_ics.py # Core-Konvertierungslogik
├── gui_wxpython.py # GUI-Version (wxPython)
├── menu.py # CLI-Menü
├── start_gui.sh/cmd # GUI-Startskripte
├── start.sh/cmd # CLI-Startskripte
├── README.md # Diese Datei
└── WXPYTHON_README.md # GUI-spezifische Dokumentation
```
### Technische Spezifikationen
- **Abhängigkeiten**: pdfplumber, icalendar, pytz, pypdf2, packaging
- **Optional für GUI**: wxPython (native Oberfläche)
- **Python-Version**: 3.6+
- **Format**: iCalendar 2.0 (RFC 5545)
- **Konfiguration**: `~/.pdf_to_ics_config.json` (GUI-Einstellungen)
## Lizenz
Dieses Tool ist zur privaten Verwendung gedacht.
--- ---
## 📚 Weitere Dokumentation ## 📚 Weitere Dokumentation
- **[WXPYTHON_README.md](WXPYTHON_README.md)** - Ausführliche GUI-Dokumentation und wxPython-Hinweise - [INSTALL.md](INSTALL.md) - Linux-Installation mit Menüeintrag
- **[QUICKSTART.md](QUICKSTART.md)** - Schnellanleitung für den Import in verschiedene Kalender - [WXPYTHON_README.md](WXPYTHON_README.md) - wxPython-spezifische Hinweise
- **[ZUSAMMENFASSUNG.md](ZUSAMMENFASSUNG.md)** - Projekt-Übersicht und technische Details - [WEB_README.md](WEB_README.md) - Web-Version (Browser/Mobil)
- [BUILD_STANDALONE.md](BUILD_STANDALONE.md) - Standalone-Builds/Packaging
- [QUICKSTART.md](QUICKSTART.md) - Kurzanleitung Kalender-Import
- [ZUSAMMENFASSUNG.md](ZUSAMMENFASSUNG.md) - Projekt- und Changelog-Übersicht
## Lizenz
Dieses Tool ist zur privaten Verwendung gedacht.

259
WEB_README.md Normal file
View File

@@ -0,0 +1,259 @@
# 🌐 Web-Version (MVP)
Diese Variante stellt den PDF-zu-ICS-Konverter im Browser bereit, damit die Nutzung auch auf mobilen Geräten möglich ist.
## Starten
### Linux/macOS
```bash
./start_web.sh
```
### Windows
Doppelklick auf `start_web.cmd`
Danach im Browser öffnen:
- Lokal: `http://localhost:8000`
- Im Netzwerk (z. B. Smartphone): `http://<IP-des-Rechners>:8000`
## Docker (Server ohne VPN)
Diese Variante ist für deinen aktuellen Wunsch geeignet: öffentlich erreichbar ohne VPN.
### 1) Starten
```bash
docker compose up -d --build
```
Aufruf:
- Direkt per IP/Port: `http://<SERVER-IP>:8000`
- Oder mit Domain über Reverse Proxy (empfohlen)
### 2) Status und Logs
```bash
docker compose ps
docker compose logs -f pdf-to-ics-web
```
### 3) Stoppen / Update
```bash
docker compose down
git pull
docker compose up -d --build
```
### 4) Optional: App-Login aktivieren
In `docker-compose.yml` die beiden Variablen aktivieren:
```yaml
environment:
- WEB_AUTH_USER=kalender
- WEB_AUTH_PASSWORD=BitteSicheresPasswortSetzen
```
Dann neu starten:
```bash
docker compose up -d --build
```
Hinweis: Ohne VPN ist mindestens HTTPS + Basic Auth empfohlen, wenn die App öffentlich im Internet hängt.
## Funktionen
- PDF-Datei hochladen
- Optional Ruhetage ausschließen
- Optional Urlaub ausschließen
- ICS-Datei direkt herunterladen
## Hinweise für mobile Nutzung
- Smartphone und Server müssen im gleichen Netzwerk sein (lokaler Betrieb)
- Bei Internet-Betrieb sollte HTTPS und ein Reverse Proxy (z. B. Nginx) genutzt werden
- Hochgeladene Dateien werden nur temporär verarbeitet
## Technischer Aufbau
- `web/app.py` FastAPI-Backend + Upload/Download-Endpunkte
- `web/templates/index.html` mobile Web-Oberfläche
- `web/requirements-web.txt` Web-spezifische Abhängigkeiten
## Produktion (Kurz)
Beispiel mit Uvicorn direkt:
```bash
.venv/bin/python -m uvicorn web.app:app --host 0.0.0.0 --port 8000
```
Optional mit App-Auth (zusätzliche Schutzschicht):
```bash
WEB_AUTH_USER=kalender WEB_AUTH_PASSWORD='StarkesPasswort' \
.venv/bin/python -m uvicorn web.app:app --host 0.0.0.0 --port 8000
```
Empfohlen für Internet-Betrieb:
- Uvicorn hinter Nginx
- HTTPS aktivieren
- Upload-Größenlimit setzen
- Zugriff absichern (z. B. Basic Auth oder Login)
## App-Auth (optional, zusätzlich zu Nginx)
Wenn `WEB_AUTH_USER` und `WEB_AUTH_PASSWORD` gesetzt sind, schützt die App alle Endpunkte per HTTP Basic Auth.
Linux/macOS Beispiel:
```bash
export WEB_AUTH_USER=kalender
export WEB_AUTH_PASSWORD='StarkesPasswort'
./start_web.sh
```
Windows (PowerShell) Beispiel:
```powershell
$env:WEB_AUTH_USER='kalender'
$env:WEB_AUTH_PASSWORD='StarkesPasswort'
./start_web.cmd
```
Hinweis: Für öffentlich erreichbare Server weiterhin Nginx + HTTPS verwenden.
## Öffentliches Deployment (HTTPS)
Beispiel für Ubuntu-Server mit Domain `ics.example.de`.
### 1) App als Service starten
`/etc/systemd/system/pdf-to-ics-web.service`
```ini
[Unit]
Description=PDF to ICS Web
After=network.target
[Service]
User=www-data
WorkingDirectory=/opt/pdf_to_ics
ExecStart=/opt/pdf_to_ics/.venv/bin/python -m uvicorn web.app:app --host 127.0.0.1 --port 8000
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
```
Aktivieren:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now pdf-to-ics-web
sudo systemctl status pdf-to-ics-web
```
### 2) Nginx als Reverse Proxy
`/etc/nginx/sites-available/pdf-to-ics`
```nginx
server {
listen 80;
server_name ics.example.de;
client_max_body_size 10M;
auth_basic "Geschuetzter Bereich";
auth_basic_user_file /etc/nginx/.htpasswd-pdf-to-ics;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Aktivieren:
```bash
sudo ln -s /etc/nginx/sites-available/pdf-to-ics /etc/nginx/sites-enabled/pdf-to-ics
sudo nginx -t
sudo systemctl reload nginx
```
### 2b) Basic Auth einrichten (empfohlen)
```bash
sudo apt-get update
sudo apt-get install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd-pdf-to-ics kalender
sudo nginx -t
sudo systemctl reload nginx
```
Weitere Nutzer hinzufügen (ohne `-c`):
```bash
sudo htpasswd /etc/nginx/.htpasswd-pdf-to-ics weiterer_user
```
Schnelltest:
```bash
curl -I https://ics.example.de
```
Erwartung: zuerst `401 Unauthorized`, mit Login im Browser dann Zugriff.
### 2c) IP-Whitelist (optional, zusätzlich)
Wenn nur bestimmte Netze zugreifen sollen, kann Nginx den Zugriff auf IP-Bereiche begrenzen.
Beispiel (lokales Netz + einzelne feste IP):
```nginx
location / {
allow 192.168.178.0/24;
allow 203.0.113.10;
deny all;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
Danach prüfen und neu laden:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
Kombiniert mit Basic Auth ist das eine robuste Mindestabsicherung.
### 3) HTTPS mit Let's Encrypt
```bash
sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d ics.example.de
```
Test der Erneuerung:
```bash
sudo certbot renew --dry-run
```
### 4) Mindest-Sicherheit
- Zugriffe absichern (mindestens Basic Auth)
- Optional zusätzlich per IP-Whitelist einschränken
- Upload-Limit klein halten (`client_max_body_size`)
- Server und Pakete regelmäßig aktualisieren

View File

@@ -164,6 +164,23 @@ Jedes Event in der ICS-Datei:
## 📝 Changelog (März 2026) ## 📝 Changelog (März 2026)
### v1.2.2
- README grundlegend strukturiert und bereinigt
- Installationshinweise nach Betriebssystem ergänzt (Linux, macOS, Windows)
- Standalone-Hinweise im README klar hervorgehoben
- Schnellstart und Troubleshooting kompakter und ohne Redundanzen
### v1.2.1
- Standalone-Build-Workflow mit PyInstaller ergänzt (`build/build_*.{sh,cmd}`)
- Packaging-Skripte für Release-Artefakte ergänzt:
- Linux: `.tar.gz`
- macOS: `.zip`
- Windows: `.zip`
- Neue Build-Dokumentation `BUILD_STANDALONE.md`
- `.gitignore` um Build-Artefakte erweitert (`dist/`, `release/`, `*.spec`, `build/PDFtoICS/`)
### v1.2.0 ### v1.2.0
- GUI-Technik vollständig auf **wxPython** umgestellt - GUI-Technik vollständig auf **wxPython** umgestellt

27
build/build_linux.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PYTHON_BIN="${PYTHON_BIN:-.venv/bin/python}"
if [ ! -x "$PYTHON_BIN" ]; then
echo "❌ Python nicht gefunden: $PYTHON_BIN"
echo "💡 Erwartet wird eine Virtual Environment unter .venv"
exit 1
fi
echo "🐧 Erstelle Linux-Standalone mit PyInstaller..."
"$PYTHON_BIN" -m pip install --upgrade pip pyinstaller
"$PYTHON_BIN" -m PyInstaller \
--noconfirm \
--clean \
--name "PDFtoICS" \
--windowed \
--add-data "version.txt:." \
gui_wxpython.py
echo "✅ Fertig: dist/PDFtoICS"

27
build/build_macos.sh Normal file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PYTHON_BIN="${PYTHON_BIN:-.venv/bin/python}"
if [ ! -x "$PYTHON_BIN" ]; then
echo "❌ Python nicht gefunden: $PYTHON_BIN"
echo "💡 Erwartet wird eine Virtual Environment unter .venv"
exit 1
fi
echo "🍎 Erstelle macOS-Standalone mit PyInstaller..."
"$PYTHON_BIN" -m pip install --upgrade pip pyinstaller
"$PYTHON_BIN" -m PyInstaller \
--noconfirm \
--clean \
--name "PDFtoICS" \
--windowed \
--add-data "version.txt:." \
gui_wxpython.py
echo "✅ Fertig: dist/PDFtoICS.app"

24
build/build_windows.cmd Normal file
View File

@@ -0,0 +1,24 @@
@echo off
setlocal
cd /d "%~dp0\.."
set "PYTHON_BIN=.venv\Scripts\python.exe"
if not exist "%PYTHON_BIN%" (
echo ❌ Python nicht gefunden: %PYTHON_BIN%
echo 💡 Erwartet wird eine Virtual Environment unter .venv
exit /b 1
)
echo 🪟 Erstelle Windows-Standalone mit PyInstaller...
"%PYTHON_BIN%" -m pip install --upgrade pip pyinstaller
"%PYTHON_BIN%" -m PyInstaller ^
--noconfirm ^
--clean ^
--name "PDFtoICS" ^
--windowed ^
--add-data "version.txt;." ^
gui_wxpython.py
echo ✅ Fertig: dist\PDFtoICS\PDFtoICS.exe

24
build/package_linux.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
APP_DIR="dist/PDFtoICS"
VERSION="$(tr -d '[:space:]' < version.txt)"
OUT_DIR="release"
ARCHIVE_NAME="PDFtoICS-linux-v${VERSION}.tar.gz"
if [ ! -d "$APP_DIR" ]; then
echo "❌ Build-Ordner nicht gefunden: $APP_DIR"
echo "💡 Bitte zuerst ausführen: ./build/build_linux.sh"
exit 1
fi
mkdir -p "$OUT_DIR"
echo "📦 Erstelle Archiv: $OUT_DIR/$ARCHIVE_NAME"
tar -czf "$OUT_DIR/$ARCHIVE_NAME" -C dist PDFtoICS
echo "✅ Fertig: $OUT_DIR/$ARCHIVE_NAME"

25
build/package_macos.sh Normal file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
APP_BUNDLE="dist/PDFtoICS.app"
VERSION="$(tr -d '[:space:]' < version.txt)"
OUT_DIR="release"
ARCHIVE_NAME="PDFtoICS-macos-v${VERSION}.zip"
if [ ! -d "$APP_BUNDLE" ]; then
echo "❌ App-Bundle nicht gefunden: $APP_BUNDLE"
echo "💡 Bitte zuerst ausführen: ./build/build_macos.sh"
exit 1
fi
mkdir -p "$OUT_DIR"
echo "📦 Erstelle Archiv: $OUT_DIR/$ARCHIVE_NAME"
rm -f "$OUT_DIR/$ARCHIVE_NAME"
(cd dist && zip -r "../$OUT_DIR/$ARCHIVE_NAME" "PDFtoICS.app" >/dev/null)
echo "✅ Fertig: $OUT_DIR/$ARCHIVE_NAME"

27
build/package_windows.cmd Normal file
View File

@@ -0,0 +1,27 @@
@echo off
setlocal
cd /d "%~dp0\.."
set /p VERSION=<version.txt
set "VERSION=%VERSION: =%"
if not exist "dist\PDFtoICS\PDFtoICS.exe" (
echo ❌ Build-Ausgabe nicht gefunden: dist\PDFtoICS\PDFtoICS.exe
echo 💡 Bitte zuerst ausfuehren: build\build_windows.cmd
exit /b 1
)
if not exist "release" mkdir release
set "ARCHIVE=release\PDFtoICS-windows-v%VERSION%.zip"
if exist "%ARCHIVE%" del /q "%ARCHIVE%"
echo 📦 Erstelle Archiv: %ARCHIVE%
powershell -NoProfile -Command "Compress-Archive -Path 'dist\PDFtoICS\*' -DestinationPath '%ARCHIVE%'"
if errorlevel 1 (
echo ❌ Konnte Archiv nicht erstellen
exit /b 1
)
echo ✅ Fertig: %ARCHIVE%

12
docker-compose.yml Normal file
View File

@@ -0,0 +1,12 @@
services:
pdf-to-ics-web:
build: .
container_name: pdf-to-ics-web
restart: unless-stopped
ports:
- "8000:8000"
environment:
- TZ=Europe/Berlin
# Optional aktivieren für App-Login:
# - WEB_AUTH_USER=kalender
# - WEB_AUTH_PASSWORD=BitteSicheresPasswortSetzen

22
start_web.cmd Normal file
View File

@@ -0,0 +1,22 @@
@echo off
REM PDF zu ICS Web-MVP starten (Windows)
setlocal enabledelayedexpansion
cd /d "%~dp0"
if not exist ".venv" (
echo 📦 Python-Umgebung wird eingerichtet...
py -3 -m venv .venv --upgrade-deps
if errorlevel 1 (
python -m venv .venv --upgrade-deps
)
)
.venv\Scripts\python.exe -c "import fastapi" 2>nul
if errorlevel 1 (
echo 📚 Installiere Web-Abhängigkeiten...
call .venv\Scripts\python.exe -m pip install -q -r web\requirements-web.txt
)
echo 🌐 Starte Web-App auf http://0.0.0.0:8000
call .venv\Scripts\python.exe -m uvicorn web.app:app --host 0.0.0.0 --port 8000

31
start_web.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
# PDF zu ICS Web-MVP starten (Linux/macOS)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
PYTHON_CMD=""
if command -v python3 &> /dev/null; then
PYTHON_CMD="python3"
elif command -v python &> /dev/null; then
PYTHON_CMD="python"
else
echo "❌ Fehler: Python nicht gefunden!"
exit 1
fi
if [ ! -d ".venv" ]; then
echo "📦 Python-Umgebung wird eingerichtet..."
$PYTHON_CMD -m venv .venv --upgrade-deps || exit 1
fi
PYTHON_VENV=".venv/bin/python"
if ! $PYTHON_VENV -c "import fastapi" 2>/dev/null; then
echo "📚 Installiere Web-Abhängigkeiten..."
$PYTHON_VENV -m pip install -q -r web/requirements-web.txt || exit 1
fi
echo "🌐 Starte Web-App auf http://0.0.0.0:8000"
exec $PYTHON_VENV -m uvicorn web.app:app --host 0.0.0.0 --port 8000

View File

@@ -1 +1 @@
1.2.0 1.2.2

0
web/__init__.py Normal file
View File

132
web/app.py Normal file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
Web-MVP für PDF zu ICS Konverter
"""
import re
import sys
import tempfile
from pathlib import Path
import os
import secrets
from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile, status
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.templating import Jinja2Templates
from starlette.background import BackgroundTask
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from pdf_to_ics import create_ics_from_dienstplan, extract_dienstplan_data
app = FastAPI(title="PDF zu ICS Web")
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
security = HTTPBasic()
def require_auth(credentials: HTTPBasicCredentials = Depends(security)):
expected_user = os.getenv("WEB_AUTH_USER")
expected_password = os.getenv("WEB_AUTH_PASSWORD")
if not expected_user or not expected_password:
return
valid_user = secrets.compare_digest(credentials.username, expected_user)
valid_password = secrets.compare_digest(credentials.password, expected_password)
if not (valid_user and valid_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unauthorized",
headers={"WWW-Authenticate": "Basic"},
)
@app.get("/", response_class=HTMLResponse)
def index(request: Request, _: None = Depends(require_auth)):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"error": None,
},
)
@app.post("/convert")
async def convert_pdf(
request: Request,
file: UploadFile = File(...),
exclude_rest: bool = Form(False),
exclude_vacation: bool = Form(False),
_: None = Depends(require_auth),
):
filename = file.filename or "dienstplan.pdf"
if not filename.lower().endswith(".pdf"):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"error": "Bitte eine PDF-Datei hochladen.",
},
status_code=400,
)
data = await file.read()
if not data:
return templates.TemplateResponse(
"index.html",
{
"request": request,
"error": "Die Datei ist leer.",
},
status_code=400,
)
with tempfile.TemporaryDirectory(prefix="pdf_to_ics_web_") as temp_dir:
temp_dir_path = Path(temp_dir)
pdf_path = temp_dir_path / "upload.pdf"
pdf_path.write_bytes(data)
dienstplan = extract_dienstplan_data(str(pdf_path))
if not dienstplan.get("events"):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"error": "Keine Dienstplan-Events in der PDF gefunden.",
},
status_code=422,
)
safe_name = re.sub(r"[^A-Za-z0-9._-]", "_", Path(filename).stem)
ics_filename = f"{safe_name}.ics"
ics_path = temp_dir_path / ics_filename
create_ics_from_dienstplan(
dienstplan,
str(ics_path),
exclude_rest=exclude_rest,
exclude_vacation=exclude_vacation,
)
content = ics_path.read_bytes()
response_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".ics", prefix="pdf_to_ics_out_")
response_tmp.write(content)
response_tmp.close()
return FileResponse(
path=response_tmp.name,
media_type="text/calendar",
filename=ics_filename,
headers={"Cache-Control": "no-store"},
background=BackgroundTask(lambda path: os.remove(path) if os.path.exists(path) else None, response_tmp.name),
)
@app.get("/health")
def health(_: None = Depends(require_auth)):
return {"status": "ok"}

9
web/requirements-web.txt Normal file
View File

@@ -0,0 +1,9 @@
fastapi>=0.115.0
uvicorn>=0.30.0
jinja2>=3.1.0
python-multipart>=0.0.9
pdfplumber
icalendar
pytz
pypdf2
packaging

120
web/templates/index.html Normal file
View File

@@ -0,0 +1,120 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PDF zu ICS (Web)</title>
<style>
:root { color-scheme: light; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
background: #f5f7fb;
color: #1f2937;
}
.wrap {
max-width: 520px;
margin: 0 auto;
padding: 20px 14px 28px;
}
.card {
background: #ffffff;
border-radius: 14px;
padding: 18px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
h1 {
margin: 0 0 10px;
font-size: 1.3rem;
}
p {
margin: 0 0 16px;
color: #4b5563;
line-height: 1.4;
}
label {
display: block;
font-weight: 600;
margin: 12px 0 8px;
}
input[type=file] {
width: 100%;
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 10px;
background: #fff;
box-sizing: border-box;
}
.options {
margin: 12px 0;
display: grid;
gap: 8px;
}
.option {
display: flex;
gap: 10px;
align-items: center;
font-weight: 500;
color: #111827;
}
button {
width: 100%;
border: 0;
border-radius: 10px;
padding: 12px;
font-size: 1rem;
font-weight: 700;
background: #2563eb;
color: #fff;
margin-top: 6px;
cursor: pointer;
}
button:active { transform: translateY(1px); }
.error {
margin: 0 0 12px;
padding: 10px;
border-radius: 10px;
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
.hint {
margin-top: 12px;
font-size: 0.9rem;
color: #6b7280;
}
</style>
</head>
<body>
<main class="wrap">
<section class="card">
<h1>PDF zu ICS Konverter</h1>
<p>PDF hochladen, konvertieren und die ICS-Datei direkt herunterladen.</p>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form action="/convert" method="post" enctype="multipart/form-data">
<label for="file">Dienstplan-PDF</label>
<input id="file" name="file" type="file" accept="application/pdf,.pdf" required />
<div class="options">
<label class="option">
<input type="checkbox" name="exclude_rest" value="true" />
Ruhetage ausschließen
</label>
<label class="option">
<input type="checkbox" name="exclude_vacation" value="true" />
Urlaub ausschließen
</label>
</div>
<button type="submit">ICS erstellen</button>
</form>
<div class="hint">Hinweis: Die Datei wird nur temporär für die Konvertierung verarbeitet.</div>
</section>
</main>
</body>
</html>