baustelle-pwa/README.md
Eddy afc6ca7046
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 13s
Große Knöpfe mit Symbol linksbündig [deploy]
Eddy: „im Auftrag … die Symbole alle linksbündig" — die farbigen Kreise
stehen in einer Spalte, der Text beginnt immer an derselben Stelle.
Gilt für alle Knöpfe mit Symbol; der kleine „Auswählen"-Knopf bleibt mittig.
Im Android-Emulator angesehen, Eddy: „lass das mal so".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:25:15 +02:00

232 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Baustelle PWA
Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und Textnotizen, Skizzen, offline-fähig. Spricht die REST-API des Dolibarr-Bericht-Moduls.
## Stack
- **Vanilla JavaScript** — kein Build, keine Framework-Abhängigkeit
- **Service Worker** für App-Shell-Cache (Workbox-frei, eigene Implementierung)
- **IndexedDB** für die Offline-Upload-Queue (Auth-Session steckt im HttpOnly-Cookie, nicht in IndexedDB)
- **REST API** des `bericht`-Dolibarr-Moduls unter `/custom/bericht/api/`
## Features
**Aufträge**
- ✅ Login mit Dolibarr-Credentials → zentrales SSO (HttpOnly-Cookie `awl_sso`, same-origin; Single-Sign-On/-Logout über alle AWL-Apps). WebAuthn/Passkey-Login möglich
- ✅ **„Heute"-Ansicht**: offene Aufträge des Tages, gefiltert aus der normalen Auftragsliste, mit Route
für alle Termine auf einen Blick (öffnet Google Maps mit allen Adressen als Wegpunkten)
- ✅ Auftragsliste mit Suche, gefiltert auf eigene Aufträge (Multi-User)
- ✅ Toggle "Auch abgeschlossene" (Filter in localStorage persistiert)
- ✅ Auftragsdetail mit Kunde, Adresse, Telefon (Click-to-Call)
- ✅ **Neuer Auftrag** (FAB): Kunde per Suche/Zuletzt-verwendet wählen, dann **Ihr Zeichen** (Pflicht), **Auftragsbeschreibung** (optional), **Geplanter Liefertermin** (Datum + Uhrzeit) und Checkbox **Auftrag direkt freigeben**. Kunden-Defaults (Zahlungsbedingung/Zahlart, Ansprechpartner) werden übernommen. Bei „direkt freigeben" wird der Auftrag **ohne Position** validiert — die Leistungen kommen aus dem Stundenzettel. (Bis Bericht 1.3.1 hängte das Backend hier eine Std-Lohn-Zeile mit Menge 1 an; die verfälschte den Auftragswert, stand als offene Restmenge in der Lieferauflistung und im Kunden-PDF.)
**Aussehen:** Knöpfe tragen farbige Symbole statt Emojis (`icon()` in app.js, Material Icons; Farben je Symbol in app.css `.ikon-<name>`) — Emojis erscheinen je nach Handy als Kästchen. Große Knöpfe: weißes Symbol im farbigen runden Feld, linksbündig (Symbole in einer Spalte, Text darunter bündig); kleine Knöpfe: farbiges Symbol. Untere Leiste: farbiges Symbol über dem Wort. Überschriften fett in normaler Schreibweise, ohne Versalien und ohne Emoji (ROADMAP Punkt 12).
**Foto- und Video-Upload**
- ✅ Live-Kamera-Modal (getUserMedia): bleibt für Serien-Aufnahmen offen — Auslöser + Filmstreifen mit Upload-Status je Foto (💾 gesichert → ⏳ lädt hoch → ✓ fertig), Front-/Rückkamera-Umschaltung (nur bei zwei Kameras), Fehlschuss löschbar
- ✅ **Volle Kameraauflösung**: ausgelöst wird über `ImageCapture.takePhoto()` (native JPEG der Kamera inkl. EXIF) — ein Standbild aus dem Video-Stream hat nur Stream-Auflösung und ist nur noch die Rückfallebene (iOS/Safari, ältere Browser)
- ✅ **Blitz und Fokus** (19.10.2026): Knopf oben schaltet reihum Aus → Auto → An → 🔦 Licht (nur was das Gerät meldet, Auswahl bleibt gemerkt; Blitz wirkt über `takePhoto({fillLightMode})`). Antippen stellt den Autofokus auf diese Stelle (Tippstelle wird in Sensorlage zurückgedreht — Chrome tut das nicht; ein echtes „Fokus festhalten“ bietet Chrome auf Android nicht an). Frontkamera/Tablet-Webcam ohne diese Fähigkeiten: kein Knopf, Antippen ohne Wirkung. Knopf „Kamera wechseln“ (weißes Symbol) nur bei mindestens zwei Kameras. Im Android-Emulator per `dumpsys media.camera` gegengeprüft (ROADMAP Punkt 9)
- ✅ **Videos** (19.10.2026): Umschalter „Foto | Video" in der Kamera, roter Aufnahmeknopf, Laufzeit „● 0:12 / 5:00", Ton (Mikrofon erst beim ersten Video angefragt), Licht/Zoom/Antippen-Fokus auch beim Filmen. MP4 (H.264/AAC) wenn möglich, sonst WebM, 5 Mbit/s; Höchstlänge 5 min bzw. passend zur Server-Grenze (`config.php upload_max`). Schließen oder App-Wechsel während der Aufnahme sichert das Video. Am Auftrag eigener Abschnitt „🎬 Videos" mit erstem Bild, Player (spult per Range), Teilen, Löschen. Galerie und Teilen nehmen Videos; zu große lehnt die App gleich ab. Server: Bericht ≥ 1.8.0 (ROADMAP Punkt 11)
- ✅ **Zoom mit zwei Fingern** im Sucher, Anzeige „1×/2,5×" (Antippen → 1×); Antippen setzt auch Belichtung und Weißabgleich auf die Stelle
- ✅ **Verkleinerte Fotos heißen `.jpg`** und transparente Bilder bekommen beim Verkleinern weißen Grund (vorher behielt ein verkleinertes AVIF/WebP seinen Namen und wurde vom Server abgelehnt)
- ✅ Alternativ Aufnahme aus der Galerie, Fotos und Videos (Mehrfachauswahl: alle Dateien werden sofort nach der Auswahl eingelesen, erst dann hochgeladen, weil Android die Galerie-Dateien nur kurz lesbar hält, siehe ROADMAP Punkt 7)
- ✅ **Fotoqualität im Admin einstellbar** (Bericht-Modul → Fotoqualität, `api/config.php`): Vorgabe ist **1:1, gar nicht verkleinern**; optional längste Seite + JPEG-Qualität. Gesichert wird immer zuerst das Original, die kleine Fassung ersetzt es danach — hängt das Verkleinern (Display aus), bleibt das Original
- ✅ **Wake Lock** während Foto-Serie und Upload: schaltet sich das Display ab, friert der Browser laufende Uploads und das Verkleinern ein
- ✅ **Persist-First / Datenverlust-Schutz**: jedes Foto wird beim Auslösen ZUERST in IndexedDB gesichert, Upload erst danach; Queue-Item wird nur nach bestätigtem Upload (HTTP 2xx mit gültigem `relpath`) gelöscht — überlebt fehlendes/schwaches Netz, hängende Uploads (Zeitlimit 45 s + 1 s je 100 KB, gleiche Formel in `api.js` und `sw.js`) und App-Kill. Gesichert wird der Blob selbst (Chrome legt ihn als Datei ab), nicht mehr ein ArrayBuffer. Erkennt 2xx-HTML (abgelaufene Session/Proxy-Loginseite) als Fehler statt als Erfolg
- ✅ Auto-Sync bei "online", periodisch (15s) und bei App-Fokus; Status-Badge (🟢 alles gesichert / 🟡 lädt hoch / 🔴 offline / ⚠️ fehlgeschlagen)
- ✅ **Background Sync**: der Service Worker lädt die Warteschlange auch dann hoch, wenn die App gar nicht offen ist (Display aus, weggewischt) — Tag `photo-queue`, dazu Periodic Background Sync (15 min), wo der Browser ihn gewährt. Anmeldung läuft über das `awl_sso`-Cookie mit
- ✅ **Wiederholter Upload ist idempotent**: geht die Antwort nach dem Speichern verloren, wird erneut gesendet — der Server vergleicht den Inhalt (md5, seit Bericht 1.6.0), legt nichts doppelt ab und antwortet mit `duplicate: true`
- ✅ Recovery: Tipp auf das Status-Badge öffnet die Warteschlange → erneut senden / teilen; Warnung beim Schließen mit noch ungesicherten Fotos
- ✅ **Seite und Service Worker laden nichts doppelt hoch**: wer ein Foto sendet, belegt es in IndexedDB (Lease = Zeitlimit + 45 s, mind. 90 s, steht als `lease_ms` im Eintrag); der andere überspringt es und übernimmt erst nach Ablauf. Beim Verkleinern bleibt das Foto belegt, bis die kleine Fassung in der Queue liegt — sonst gingen Original **und** kleine Fassung raus. Logik steht zweimal (`lib/idb.js` + `sw.js`), immer zusammen ändern
- ✅ **App-Update lädt nicht mitten in der Arbeit neu**: nach einem Deploy wartet der Reload, bis kein Dialog (Kamera, Notiz, Unterschrift) offen ist, nichts getippt wird und kein Upload läuft (`window.appBusy()`); der eigene Reload löst keinen Browser-Dialog aus
- ✅ **Wartende Fotos stehen am Auftrag**: Block „⏳ Warten auf Upload (n)" mit Vorschaubildern aus der Warteschlange und Klartext (gesichert auf dem Gerät, geht raus sobald Netz da ist); nach dem Upload zieht die Auftragsseite von selbst nach. Hinweis-Toast beim Schließen der Kamera, wenn noch etwas wartet
- ✅ Kacheln laden **serverseitige Thumbnails** (`photo.php?size=thumb`) statt der Originale — rund 10 KB statt mehrerer hundert KB je Kachel, parallel und mit `loading="lazy"`, Wiederholaufrufe enden mit 304
- ✅ Foto-Viewer mit Zoom + Swipe
- ✅ Foto-Skizze: Annotationen mit Pfeilen, Kreisen, Rechtecken, Text
**Notizen**
- ✅ Getippte Notiz zum Auftrag: Betreff + Text, Liste mit Vorschau und Zeitstempel, jederzeit wieder les- und änderbar
- ✅ Ablage als `notiz_<betreff>_<datum>.txt` im Auftragsordner — damit auch im Dolibarr-Auftrag und in der Anhänge-Spalte des Bericht-Editors sichtbar
- ✅ Löschen mit Rückfrage; in „Weitere Dokumente" tauchen Notizen nicht doppelt auf
**Sprachnotizen**
- ✅ Sprachaufnahme via MediaRecorder (webm)
- ✅ Whisper-Transkription (serverseitig, API-Aufruf)
**Berichte**
- ✅ Berichte pro Auftrag anzeigen
- ✅ Seitenkacheln zeigen das im Editor gebaute Seitenbild (`composite_path`) samt Anmerkungen — nicht mehr nur das Rohbild; Raster-Layouts sind mit „⊞ 4" gekennzeichnet
- ✅ Bericht erstellen mit Template-Auswahl
- ✅ Seiten umordnen per Drag&Drop
- ✅ Seite löschen, Notiz bearbeiten
- ✅ Touch-Unterschrift abnehmen (mit GPS + Name)
- ✅ Bericht finalisieren → PDF-Vorschau
- ✅ PDF-Download
**Lieferungen + Kundenunterschrift**
- ✅ Lieferungen-Liste pro Auftrag mit Status + Signed-Badge
- ✅ Lieferschein-PDF inline (PDF.js, alle Seiten)
- ✅ Vollbild-Querformat-Unterschrift mit HiDPI-Canvas, transparent (kein Hintergrund)
- ✅ Trimmt Canvas automatisch auf bemalte Fläche
- ✅ Name des Unterzeichners aus Kundendaten vorausgefüllt
- ✅ GPS-Koordinaten (optional, timeout 3s)
- ✅ Unterschrift wird via ODT-Hook in das Lieferschein-PDF gestempelt
- ✅ Expedition wird nach Unterschrift automatisch validiert + geschlossen
- ✅ Signed-PDF direkt nach Bestätigung als Vorschau
**Materialliste**
- ✅ Material pro Auftrag erfassen (Label, Menge, Einheit, Notiz)
- ✅ Material löschen
**PWA**
- ✅ Service Worker für Offline-Start
- ✅ Installierbar als PWA (Home-Screen-Icon)
- ✅ **Web Share Target** — aus WhatsApp & Co. Bilder **und** Beschreibungstext an „Baustelle" teilen:
anmelden (falls nötig, direkt in der Teilen-Seite), Auftrag suchen und wählen, ablegen. Bilder gehen
über die normale Upload-Queue, der Text wird als Notiz (`notiz_*.txt`) im Auftrag gespeichert
- ✅ **Serie ohne Sucherei**: die Teilen-Seite merkt sich die letzten drei benutzten Aufträge und zeigt
sie unter „Zuletzt benutzt" ganz oben. Wurde einer davon in den letzten 30 Minuten benutzt, ist er
**vorausgewählt** — die zweite und dritte Nachricht der Kundin kosten dann nur noch einen Tipp.
Danach steht er nur noch als Schnellzugriff da, damit nichts versehentlich im Auftrag von vorgestern landet
- ✅ Nativer Android-Share-Sheet für Fotos, PDFs und Dokumente (`navigator.share`) — direkt an WhatsApp / Signal / Mail
- ✅ Mehrfachauswahl in Foto- und Dokument-Liste mit Batch-Teilen
- ✅ Android-Hardware-Back-Button schließt Modale/Ansichten sauber (History-Modal-Stack), bricht Mehrfachauswahl ab statt App zu verlassen
- ✅ „Nochmal drücken zum Beenden"-Toast auf Top-Level-Routen
- ✅ PIN-Schutz (optional)
- ✅ **Push-Benachrichtigungen via ntfy** (statt klassischem Web-Push): eigener Server/Topic in den
Einstellungen, Browser-Notification-Permission, Reconnect bei Verbindungsabbruch
- ✅ **Bild-Cache** `baustelle-media` (cache-first, LRU 300): schon angesehene Fotos sind auch ohne Netz da; überlebt Deploys, wird beim Abmelden geleert
- ✅ **Keine Browser-Dialoge** — `confirm`/`alert`/`prompt` sind durch eigene Modale ersetzt (`confirmDialog`, `alertDialog`, `inputDialog`); der Hardware-Zurück-Button schließt sie als Abbruch
**Ohne Netz** (Keller, Neubau, Funkloch — auf der Baustelle der Normalfall)
- ✅ **Datenspiegel**: Auftragsliste, Kundenliste, Berichte sowie geöffnete Auftrags- und
Kundendetails werden nach jedem erfolgreichen Abruf in IndexedDB gespiegelt und ohne Netz
von dort angezeigt (Details: die letzten 30). Zusammen mit dem Bild-Cache steht ein einmal
geöffneter Auftrag samt Fotos auch im Funkloch
- ✅ **Sichtbares Band** „📴 Kein Netz — angezeigt wird der Stand von HH:MM Uhr" — ohne den
Hinweis hält man alte Zahlen für aktuell. Kommt das Netz zurück, lädt die Ansicht neu und
das Band verschwindet
- ✅ **Suche greift auf den gespiegelten Stand zu**, statt ins Leere zu laufen (mit Hinweis)
- ✅ **Verständliche Meldungen**: ein `fetch()`, das nicht bis zum Server kommt, wird zu
„Keine Verbindung zum Server" — nicht mehr die rohe Browsermeldung „Failed to fetch".
Ein echter Serverfehler (401/403/500) wird weiterhin als solcher gemeldet
- ✅ **Start ohne Netz**: der Service Worker liefert die App-Hülle aus dem Cache; die Seite
meldet ihm nach dem Laden ihre tatsächlichen Asset-URLs zum Nachcachen, sonst wäre der
Cache direkt nach einem Deploy leer (`?v=<mtime>` verhindert eine feste Dateiliste)
- ✅ **Beim Abmelden wird der Spiegel gelöscht** — am geteilten Gerät sieht sonst der nächste
Benutzer offline die Kunden und Preise des vorherigen
- ⛔ **Ändern braucht Netz** — bewusst keine Schreib-Warteschlange für Auftragsdaten
(bräuchte Idempotenz + Konfliktbehandlung). Fotos sind die Ausnahme: die gehen über die
Persist-First-Queue und werden nachgeliefert
### PDF-Betrachter und Zurück-Knopf (2026-10-15)
- **Ein** PDF-Betrachter für alles (`openPdfModal`): Berichtsvorschau, Lieferschein (Vorschau antippen) und Dokumente am Auftrag (`openFileViewer` → `openPdfViewer` ruft ihn auf). Alle Seiten untereinander, Seitenanzeige, Zoom per Knöpfe − / + / „100 %" (Seitenbreite), **Zwei-Finger-Zoom** (Vorschau während der Geste, scharf neu gezeichnet beim Loslassen, die Stelle unter den Fingern bleibt stehen), Doppeltipp (Seitenbreite ↔ 250 %), Strg+Mausrad. Download bei Dokumenten weiterhin als Originaldatei vom Server.
- Der Zurück-Knopf oben links zeigt Pfeil **und** das Wort „Zurück" (44 px Tippfläche).
## Hosting
Deploy per Forgejo-Pipeline (Commit-Tag `[deploy]`) nach `/mnt/appdata/firma/dolibarr-202509/modules/baustelle/`. Erreichbar unter `https://awl.data-it-solution.de/custom/baustelle/`. Cache-Busting automatisch via `filemtime()` in `index.php` (`?v=<mtime>` an JS/CSS) — kein manuelles Versions-Hochzählen nötig, die PWA aktualisiert sich beim nächsten Laden.
## Verzeichnis-Struktur
```
baustelle-pwa/
├── index.php App-Shell (Cache-Busting via filemtime → ?v=<mtime>)
├── share.html Web-Share-Target-Empfänger
├── manifest.webmanifest PWA Manifest
├── sw.js Service Worker (Network-First für Assets, API pass-through,
│ cache-first für photo.php?size=thumb → Cache baustelle-media,
│ Share-Target, Background Sync der Foto-Queue inkl. Lease)
├── app.js Hauptlogik (Routes, Views, Dialoge, Notizen, Modal-Stack, appBusy)
├── app.css Mobile-first Styling
├── lib/
│ ├── idb.js IndexedDB-Wrapper: kv-Store (Benutzer, Datenspiegel) + Foto-Queue
│ │ (queuePatch/queueClaim/queueRelease — atomar, Gegenstück in sw.js)
│ ├── api.js REST-API-Client (SSO-Cookie same-origin, JSON-Guard, Timeout,
│ │ Netzfehler → `offline = true`)
│ ├── offline.js Offline-Queue + Sync (Persist-First, Lease), Wake Lock, Datenspiegel
│ ├── router.js Hash-Router (wartet vor dem Hash-Wechsel auf ausstehende history.back())
│ ├── pdf.min.js PDF.js (Lieferschein-Vorschau)
│ └── pdf.worker.min.js PDF.js Worker
├── icons/
│ ├── icon.svg
│ ├── icon-192.png
│ └── icon-512.png
├── .forgejo/workflows/ deploy.yml — rsync nach Prod bei `[deploy]`, meldet per Ntfy
├── ROADMAP.md Offene Punkte + Befunde/Verifikation der letzten Runden
└── README.md
```
**Lokal testen:** `/var/www/dolibarr/custom/baustelle` ist ein Symlink auf dieses Repo — Änderungen sind sofort unter `http://localhost:8080/custom/baustelle/` live. Nach Code-Änderungen die Seite wirklich neu laden (nicht nur den Hash wechseln), sonst läuft der alte Stand.
## API
Die App spricht die Bericht-API unter `/custom/bericht/api/`:
**Auth**
- `POST /auth.php` — Login mit Dolibarr-Credentials → setzt HttpOnly-Cookie `awl_sso` (zentrales SSO via awlauth). Folge-Requests authentisieren same-origin über das Cookie (kein Bearer-Token)
- `GET /verify.php` — prüft das Cookie und verlängert die Session (sliding renewal); läuft bei jedem Routenwechsel (`ensureAuth()`) und ist der Weg, wie `share.html` prüft, ob eine Anmeldung nötig ist
- `POST /logout.php` — löscht das `awl_sso`-Cookie (Single-Logout über alle AWL-Apps)
**Aufträge**
- `GET /orders.php` — Aufträge des Users (Filter: `?open=1`, `?q=suche`)
- `GET /orders.php?id=X` — Auftrag-Detail inkl. verknüpfte Berichte
- `GET /orders.php?id=X&action=photos` — Anhang-Liste (je Datei `taken_at` = EXIF-Aufnahmezeit, danach sortiert)
- `POST /orders.php?id=X&action=upload_photo` — Foto-Upload (multipart)
- `POST /orders.php?action=create` — Neuen Auftrag anlegen (Kunde, Ihr Zeichen, Beschreibung, Liefertermin, optional direkt freigeben)
**Kunden**
- `GET /customers.php` — Kundenliste (Filter: `?q=suche`)
- `GET /customers.php?id=X` — Kundendetail
**Berichte**
- `GET /reports.php` — Alle Berichte des Users
- `GET /reports.php?id=X` — Bericht-Detail mit Seiten (je Seite `composite_path`, `title`, `layout` — `composite_path` anzeigen, wenn gesetzt)
- `POST /reports.php?action=create` — Neuen Bericht anlegen
- `POST /reports.php?id=X&action=finalize` — Bericht finalisieren
- `DELETE /reports.php?id=X` — Bericht löschen
**Seiten**
- `DELETE /pages.php?id=X` — Seite löschen
- `POST /pages.php?id=X` — Seite aktualisieren (note, rotation)
- `POST /pages.php?action=signature&bericht_id=X` — Unterschrift hinzufügen
- `POST /pages.php?action=reorder` — Seiten umordnen
**Material**
- `GET /materials.php?element_type=X&element_id=Y` — Materialliste
- `POST /materials.php?element_type=X&element_id=Y` — Material hinzufügen
- `POST /materials.php?id=X&delete=1` — Material löschen
**Lieferungen**
- `GET /shipments.php?order_id=X` — Lieferungen zu einem Auftrag
- `GET /shipments.php?id=X` — Lieferung-Detail (inkl. bericht_id falls Unterschrift vorhanden)
- `GET /shipments.php?id=X&action=pdf[&variant=auto|signed|unsigned]` — Lieferschein-PDF
- `POST /shipments.php?id=X&action=confirm` — Kunden-Unterschrift einstempeln (FormData mit signature_png, signer_name, gps_lat, gps_lon, signed_at). Backend setzt signed_status=1 und schließt die Expedition
**Notizen**
- `GET /note.php?order_id=X` — Liste (Betreff, Vorschau, Zeitstempel)
- `GET /note.php?order_id=X&file=<name>` — eine Notiz mit vollem Text
- `POST /note.php?order_id=X` — anlegen `{ subject, text }`
- `POST /note.php?order_id=X&file=<name>` — ändern
- `POST /note.php?order_id=X&file=<name>&delete=1` — löschen
**Medien**
- `GET /photo.php?relpath=X` — Datei abrufen; Auth über `awl_sso`-Cookie (same-origin, auch für `<img>`-Tags — kein `?jwt=` mehr nötig)
- `GET /photo.php?relpath=X&size=thumb&w=<px>` — serverseitiges Thumbnail (GD, gecacht, ETag → 304). **Der Weg für Kachel-Ansichten.** `size=small` ist etwas anderes: das liefert nur ein von Dolibarr vorgefertigtes Thumb, das es für PWA-Uploads gar nicht gibt — dort kam bis Modul-Version 1.5.1 immer das Original zurück
- `POST /delete_photo.php` — Foto löschen
- `POST /voice.php?order_id=X` — Sprachnotiz hochladen
- `POST /transcribe.php` — Whisper-Transkription anfordern
- `GET /pdf.php?id=X` — PDF abrufen (Final oder Preview); Auth über `awl_sso`-Cookie (same-origin)
**Templates**
- `GET /templates.php` — Bericht-Vorlagen
- `GET /odt_templates.php` — ODT-Deckblatt-Vorlagen
**Konfiguration**
- `GET /config.php` — Admin-Vorgaben zur Fotoqualität (`photo_maxside`, `photo_quality`); wird beim Start und nach jedem Login neu geladen, zwischengespeichert in `localStorage` für den Offline-Fall
## Lizenz
GPL v3+