diff --git a/README.md b/README.md index b1ddf48..04a2fa7 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,15 @@ Interaktives Textmenü: ./start.sh ``` +### 3) Web (MVP für Mobilgeräte) +Browser-Variante mit Upload + direktem ICS-Download: + +```bash +./start_web.sh +``` + +Details siehe [WEB_README.md](WEB_README.md). + --- ## ⚡ Schnellstart (60 Sekunden) @@ -149,6 +158,7 @@ Die erzeugten `.ics`-Dateien lassen sich u. a. in folgende Kalender importieren: - [INSTALL.md](INSTALL.md) - Linux-Installation mit Menüeintrag - [WXPYTHON_README.md](WXPYTHON_README.md) - wxPython-spezifische Hinweise +- [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 diff --git a/WEB_README.md b/WEB_README.md new file mode 100644 index 0000000..02ef7ab --- /dev/null +++ b/WEB_README.md @@ -0,0 +1,49 @@ +# 🌐 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://:8000` + +## 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 +``` + +Empfohlen für Internet-Betrieb: +- Uvicorn hinter Nginx +- HTTPS aktivieren +- Upload-Größenlimit setzen +- Zugriff absichern (z. B. Basic Auth oder Login) diff --git a/start_web.cmd b/start_web.cmd new file mode 100644 index 0000000..35907f5 --- /dev/null +++ b/start_web.cmd @@ -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 diff --git a/start_web.sh b/start_web.sh new file mode 100755 index 0000000..bd45c59 --- /dev/null +++ b/start_web.sh @@ -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 diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/app.py b/web/app.py new file mode 100644 index 0000000..dc95d00 --- /dev/null +++ b/web/app.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Web-MVP für PDF zu ICS Konverter +""" + +import re +import sys +import tempfile +from pathlib import Path +import os + +from fastapi import FastAPI, File, Form, Request, UploadFile +from fastapi.responses import FileResponse, HTMLResponse +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")) + + +@app.get("/", response_class=HTMLResponse) +def index(request: Request): + 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), +): + 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(): + return {"status": "ok"} diff --git a/web/requirements-web.txt b/web/requirements-web.txt new file mode 100644 index 0000000..f697565 --- /dev/null +++ b/web/requirements-web.txt @@ -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 diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..2944f0b --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,120 @@ + + + + + + PDF zu ICS (Web) + + + +
+
+

PDF zu ICS Konverter

+

PDF hochladen, konvertieren und die ICS-Datei direkt herunterladen.

+ + {% if error %} +
{{ error }}
+ {% endif %} + +
+ + + +
+ + +
+ + +
+ +
Hinweis: Die Datei wird nur temporär für die Konvertierung verarbeitet.
+
+
+ +