105 Commits

Author SHA1 Message Date
Pepe Ziberi
f9ad00a7e7 fix(journal): Modul-Button folgt dem Mandanten des offenen Einsatzes (v1.8.6)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 45m49s
Ursache: der "Module"-Button hing am Mandanten des Nutzers, nicht am Mandanten des
geöffneten Einsatzes. SERVER_ADMIN (ohne eigenen Mandant) oder mandantenfremde Einsätze
zeigten daher keinen Button.

- /api/tenant/modules: GET/PUT akzeptieren tenantId; SERVER_ADMIN darf den Mandanten des
  Einsatzes verwalten, andere nur den eigenen (canManage sauber berechnet)
- journal-view lädt/speichert Module für den Einsatz-Mandanten (projectTenantId)
- ModuleManagerDialog reicht tenantId beim Speichern durch

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:36:04 +02:00
Pepe Ziberi
aaeeee1717 fix(journal): Modul-Button & Aktionen auf dem Handy klar sichtbar (v1.8.5)
- Journal-Kopfzeile bricht auf dem Handy sauber um (Titel oben, Aktionen darunter)
- "Module"-Button trägt jetzt immer die Beschriftung (vorher nur Icon auf dem Handy)
  und ist damit auffindbar; Aktionen (Module/Rapport/E-Mail/Drucken) umbrechend statt gequetscht

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:21:29 +02:00
Pepe Ziberi
b095d9de1c fix(admin): Mobil-Layout der Administration entzerrt (v1.8.4)
- Tab-Leiste (7 Tabs) war auf dem Handy in grid-cols-7 gequetscht/überlappend →
  jetzt horizontal scrollbar (flex/overflow-x-auto), ab md wieder 7-spaltiges Grid
- Header responsiv: "Zurück"-Text nur ab sm, Name kürzt (truncate), Rollen-Badge nur ab sm,
  kleinere Paddings

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:17:31 +02:00
Pepe Ziberi
ae78b2d878 fix(nav): Sicherheit/2FA & Konto-Datenschutz im Benutzer-Menü auffindbar (v1.8.3)
2FA lag unter /settings, war aber über kein Menü erreichbar. Neu im Benutzer-Menü
(Desktop-Dropdown + Mobil-Menü): "Sicherheit & 2FA" (→ /settings) und
"Konto & Datenschutz" (→ /konto/datenschutz).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:15:40 +02:00
Pepe Ziberi
9ba7194d41 feat(cockpit): Baukasten aufgewertet — Slot-Wahl, Zahl-Spalte, Module bearbeiten, Unwetter-Vorlage (v1.8.2)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 44m40s
Journal modularer (Feedback Test-Feuerwehr Unwettereinsätze):
- Modul-Manager: Anzeige-Bereich (Hauptbereich/Seitenspalte) frei wählbar; bestehende
  Tabellen-Module bearbeitbar (Name/Slot/Spalten), nicht nur neu erstellen
- Neuer Spaltentyp "Zahl" (z.B. Auftragsnummer) inkl. Rendering/Eingabe im table-module
- Fertige Vorlage "Einsatz-Aufträge (Unwetter)": Zeit · Auftrags-Nr · Adresse · Auftrag ·
  Zugeteilt · erledigt (Hauptbereich, gross links)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:44:38 +02:00
Pepe Ziberi
f58f1b31bf feat(auth): WebAuthn (YubiKey/Passkeys) + Admin-2FA-Reset-Button (v1.8.1)
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
- WebAuthn/FIDO2 via @simplewebauthn v13: Registrierung (Session) + Login-Faktor (öffentlich),
  zustandslose Challenge-Token (JWT), Zähler/Klon-Schutz, Credentials in webauthn_credentials
  - Routen: /api/auth/mfa/webauthn/register/{options,verify}, DELETE /webauthn/[id],
    /api/auth/mfa-login/webauthn/{options,verify}
- Settings: Sicherheitsschlüssel/Passkey hinzufügen + Liste + entfernen
- Login: "Mit Sicherheitsschlüssel/Passkey anmelden" (neben TOTP)
- Admin: 2FA-Zurücksetzen-Button in der Benutzerliste (nutzt bestehende mfa-reset-API)
- Sicherheitsseite + TOM-Doku: 2FA als vorhanden dokumentiert

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:15:03 +02:00
Pepe Ziberi
f588b6d361 feat(auth): Zwei-Faktor-Authentisierung — TOTP (Authenticator-App) (v1.8.0)
Freiwillige MFA, du kannst sie für dich aktivieren; Login verlangt danach den zweiten Faktor.

- Schema/Migration: User.totpSecret/totpConfirmedAt/mfaBackupCodes + Tabelle webauthn_credentials
  (WebAuthn folgt), otplib v12
- lib/mfa.ts: TOTP-Secret/QR-URI/Verify + bcrypt-gehashte Backup-Codes
- lib/auth.ts: buildSessionForUser, createMfaToken/verifyMfaToken (10-Min-Zwischentoken), userHasMfa
- Login-Flow: nach Passwort -> bei aktiver MFA kein Cookie, sondern mfaToken; öffentlicher
  /api/auth/mfa-login/verify prüft TOTP ODER Backup-Code und setzt dann die Session (Rate-Limit)
- Einrichtung in /settings: QR scannen, Code bestätigen, Backup-Codes (einmalig), deaktivieren
- Admin-Reset-API: /api/admin/users/[id]/mfa-reset (SERVER_ADMIN alle, TENANT_ADMIN eigene Org)
- Login-Seite: zweiter Schritt für Code/Backup-Code
- SMS bewusst weggelassen (Kosten/SIM-Swap)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:06:24 +02:00
Pepe Ziberi
fa1e911ecd feat(legal): Zustimmungs-Nachweise mit IP, Gerät/Browser & Hash (v1.7.8)
Belastbarer Einwilligungsnachweis:
- Schema/Migration: legal_acceptances + ipAddress, userAgent (contentHash bestand bereits)
- Erfassung bei Registrierung und jeder Zustimmung (/api/legal/accept)
- Admin /admin/legal: neue Spalten IP + Gerät/Browser + Hash; CSV-Export um ipAddress,
  userAgent, contentHash erweitert
- Datenschutzerklärung + Dateninventar: IP/User-Agent als dokumentierter Beweiszweck ergänzt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:55:57 +02:00
Pepe Ziberi
5cdf0e63c9 refactor(wording): "Spende/spenden" -> "Freiwilliger Unterstützungsbeitrag/Beitrag" (v1.7.7)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 45m18s
Sichtbare Texte umgestellt (Routen /spenden und Code-Namen unverändert):
- Home, Unterstützungsseite (/spenden), Danke-Seite, Layout-Titel
- Admin-Tab "Unterstützung", Texte + Button "Jetzt unterstützen"
- settings-tab Stripe-/Unterstützungs-Konfiguration
- Nutzungsbedingungen/Unterstützen: "Spendenbescheinigung" -> "Bescheinigung"
- Stripe-Checkout-Position: "Freiwilliger Unterstützungsbeitrag für Lageplan"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:53:11 +02:00
Pepe Ziberi
836b231c42 feat(legal): echte Betreiberangaben eingesetzt, Impressum als Privatperson (v1.7.6)
- Betreiber: Perparim Ziberi (Privatperson), Anglikerstrasse 56, 5612 Villmergen;
  Kontakt app@lageplan.ch, Datenschutz datenschutz@lageplan.ch
- Serverstandort/Hosting: Eigenbetrieb, Schweiz (beim Betreiber) — MinIO/PostgreSQL Region = Schweiz
- Backup-Aufbewahrung: bis zu 30 Tage · Gerichtsstand: Wohnsitz des Betreibers (Schweiz)
- Impressum: Firma "Kingstickers Ziberi" + UID entfernt (auf Wunsch Privatperson), Urheberrecht
  und Copyright auf Perparim Ziberi umgestellt; Home-Footer-Copyright angeglichen
- Alle Legal-Platzhalter damit gefüllt (keine Build-Warnung mehr)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:47:04 +02:00
Pepe Ziberi
a14753c994 feat(legal): Rechtstexte auf finale Fassung (v1.7.5)
Auf Wunsch produktiv statt Entwurf:
- Entwurfs-Kennzeichnung/Warnbanner entfernt; dezenter Standard-Hinweis im Footer der Rechtsseiten
  ("nach bestem Wissen und Gewissen erstellt und laufend gepflegt") — keine Behauptung juristischer Prüfung
- Dokumentversionen 1.0.0-draft -> 1.0.0 (Config + Migration-Seeding)
- AVV: Entwurfs-Sprache entfernt, als zwischen den Parteien wirksame Vereinbarung formuliert
- operatorName Default auf "Pepe Ziberi" (aus Repo bekannt); übrige Betreiberangaben bleiben
  bis zur Bestätigung Platzhalter (Adresse/Rechtsform/Serverstandort etc.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:28:27 +02:00
Pepe Ziberi
9eafe984e1 docs(legal): Abschlussbericht der Compliance-Umsetzung (v1.7.4)
docs/legal/IMPLEMENTATION-REPORT.md — Zusammenfassung, geänderte Dateien, DB, Konfiguration,
manuelle Schritte, offene rechtliche Entscheidungen, Test-/Build-Ergebnisse, Folgeschritte.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:19:59 +02:00
Pepe Ziberi
a943db9c54 docs(legal)+test: Compliance-Dokumentation & Consent-Unit-Tests (v1.7.4)
Teil 5 des Compliance-Updates:

- docs/legal/: data-processing-inventory.md, subprocessors.md,
  technical-organizational-measures.md, retention-and-deletion.md, incident-response.md,
  licenses-and-attributions.md — Ist-Zustand aus der Analyse, offene Punkte als Platzhalter
- Reine Consent-Regeln nach src/lib/legal-rules.ts ausgelagert (kein Prisma) + Texte nach
  src/lib/legal-content.ts (7 Pflichtbestätigungen) — für Testbarkeit
- Vitest eingerichtet (vitest.config.ts, test-Script); 13 Tests grün:
  Pflicht-Dokumenttypen, Re-Consent bei neuer Version, normale Benutzer ≠ Org-Vertreter,
  7 Org-Bestätigungen, Platzhalter-Erkennung, keine erfundenen Subprozessoren

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:19:01 +02:00
Pepe Ziberi
235959101b feat(legal): In-App-Hinweise, Export-Disclaimer & Web-Härtung (v1.7.3)
Teil 4 des Compliance-Updates:

- Export: rechtlicher Prüf-Hinweis in PDF-Footer und eingebrannt in PNG
  ("Angaben vor Verwendung fachlich prüfen · Offline-/Einsatzunterlagen bleiben massgebend")
- Dashboard: dezenter, minimierbarer Hinweisbanner (unterstützendes Werkzeug ohne garantierte
  Verfügbarkeit → /verantwortungsvolle-nutzung); bleibt in den Rechtsseiten erhalten
- Messpanel: expliziter Berechnungs-/Geodaten-Hinweis ("vor operativer Verwendung fachlich prüfen")
- Middleware: private Routen (/app, /admin, /settings, /konto) mit X-Robots-Tag noindex + no-store

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:11:43 +02:00
Pepe Ziberi
6ab2fd9a6b feat(legal): Organisationsbestätigung, Re-Consent-Gate & Admin-Ansicht (v1.7.2)
Teil 3 des Compliance-Updates — Zustimmungs-UI und Nachweis-Verwaltung:

- LegalConsentGate (blockierender Dialog): erkennt offene Pflicht-Zustimmungen nach Login,
  zeigt Organisationsbestätigung mit freundlicher Erklärung + 7 einzeln anzukreuzenden
  Pflichtbestätigungen (keine Vorauswahl) für Org-Admins, sonst Re-Consent für aktualisierte
  Nutzungsbedingungen/Datenschutz. Abmelden/Dokumente-Lesen bleiben möglich (Allowlist);
  keine stillschweigende Zustimmung. Tastatur-/Screenreader-tauglich (role=dialog)
- /konto/datenschutz: eigene Zustimmungen einsehen, Datenexport/Kontolöschung anfordern,
  Datenschutzkontakt (Route via Middleware geschützt)
- /admin/legal (nur SERVER_ADMIN): aktive Dokumentversionen, Zustimmungszahlen, fehlende
  Zustimmungen, Suche nach Benutzer, CSV-Export (minimale Nachweisdaten)
- APIs: /api/legal/my-acceptances, /api/admin/legal, /api/admin/legal/acceptances (+CSV)
- Gate ins Root-Layout eingehängt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:07:59 +02:00
Pepe Ziberi
8cf1aa4dbe feat(legal): Consent-Datenmodell, Migration & Registrierungs-Zustimmung (v1.7.1)
Teil 2 des Compliance-Updates — revisionssichere Zustimmungsprotokollierung:

- Prisma: Modelle LegalDocument (versioniert, contentHash, isActive, unique[type,version])
  und LegalAcceptance (userId, organizationId?, documentType/-Version, contentHash, context,
  unique gegen Duplikate, User-Relation)
- migrate.js: idempotente Tabellen + Indizes + Seeding/Aktivierung der 5 Rechtsdokumente
  (eine aktive Version je Typ; contentHash aus bewusster Versionsangabe)
- src/lib/legal.ts: getActiveDocuments, getPendingAcceptances, recordAcceptances (skipDuplicates,
  nie überschreiben), Pflichtsets (alle: TERMS+PRIVACY; Org-Admin zusätzlich ORGANIZATION_DECLARATION)
- API: GET /api/legal/status (offene Zustimmungen), POST /api/legal/accept (erfassen, CSRF-Check)
- Registrierung: zwei getrennte, nicht vorausgewählte Zustimmungen (Nutzungsbedingungen akzeptieren
  + Datenschutz-Kenntnisnahme), serverseitig via zod erzwungen, Version protokolliert

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:02:20 +02:00
Pepe Ziberi
872b8927cc feat(legal): Claims-Korrektur, zentrale Legal-Config & öffentliche Rechtsseiten (v1.7.0)
Teil 1 des Compliance-Updates (nach vollständiger Repo-Analyse):

- src/config/legal.ts: zentrale, typisierte Betreiber-/Rechtskonfiguration mit sichtbaren
  Platzhaltern, ENV-Overrides (LEGAL_*), Build-Warnung bei offenen Pflichtangaben, echte
  Subprozessorenliste (aus dem Code abgeleitet)
- Claims-Korrektur (20 Aussagen): 24/7, 100% Swiss Hosted, DSG/DSGVO-konform, 100% der Spende,
  Kein Unternehmen, "bleibt kostenlos", "sicher via Stripe", "keine Weitergabe/keine
  Übermittlung ins Ausland", "verschlüsselt"→"gehasht" — in page.tsx, spenden, impressum,
  datenschutz (Neuaufbau), settings-tab
- Neue Legal-Seiten (druckbar, responsiv, versioniert, als Entwurf gekennzeichnet):
  /nutzungsbedingungen, /verantwortungsvolle-nutzung, /sicherheit, /unterstuetzen,
  /organisationen/datenschutzvereinbarung + wiederverwendbare LegalPage-Shell
- Datenschutz komplett neu aufgebaut: Subprozessoren-Tabelle (#dienstleister), Auslandbekanntgabe,
  gehashte Passwörter, Backups/Löschung, Zahlung (Stripe), Karten/Geodienste, Rechte/EDÖB
- Footer um alle Pflicht-Rechtslinks erweitert; sitemap.ts ergänzt
- docs/legal/website-claims-audit.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:57:39 +02:00
Pepe Ziberi
6aa70e1504 feat(share): Nur-Ansicht-Teilen-Link (live, optional PIN) + QR (v1.6.9)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 31m39s
Neuer öffentlicher Teilen-Link für die aktuelle Lage — ohne Login nur zum
Ansehen, live aktualisiert. Optional mit 4–8-stelliger PIN geschützt.

- DB: Project.shareToken (unique) / sharePin (bcrypt) / shareEnabled + Migration
- API: POST/GET /api/projects/[id]/share (Verwaltung, Mandanten-Check, VIEWER blockiert),
  GET/POST /api/share/[token] (öffentlich, PIN-Prüfung, Rate-Limit gegen Brute-Force)
- Öffentliche Seite /view/[token]: PIN-Gate, read-only MapView, Socket-Live + 30s-Poll
- Teilen-Dialog im Werkzeuge-/Mobil-Menü: Link kopieren, QR-Code, PIN setzen/entfernen,
  Teilen beenden (widerruft den Link)
- Middleware: /api/share/ öffentlich freigeschaltet

Hinweis: hochgeladene Custom-Symbole zeigen im anonymen Viewer den Platzhalter
(Icon-Endpunkte bleiben auth-geschützt); eingebaute taktische Zeichen erscheinen normal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:30:47 +02:00
Pepe Ziberi
eb5da2f0c3 fix(map+edit): erster Zeichen-Punkt sofort sichtbar + Bearbeiten-Status klarer (v1.6.8)
- Zeichnen: gesetzte Eckpunkte (Linie/Polygon/Pfeil/Gefahrenzone) erscheinen
  sofort als Marker beim Klick — bisher sah man auf dem Handy erst ab dem
  zweiten Punkt etwas (Vorschau nur bei Mausbewegung)
- Gemeinsames Bearbeiten: alle drei Zustände explizit in der Statuszeile —
  "Ansichtsmodus frei · Bearbeiten übernehmen", "Sie bearbeiten · freigeben",
  "X bearbeitet · nur Ansicht"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:23:09 +02:00
Pepe Ziberi
471690550f fix(ui): Freigeben-Button in Topbar + Toolbar-Farben nicht mehr abgeschnitten (v1.6.7)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 32m5s
- Freigeben: prominenter grüner Button neben Speichern, sichtbar solange man
  den Bearbeitungs-Lock hält (war nur als versteckter Link in der Statuszeile)
- Linke Toolbar kompakter (flachere Buttons/Trenner, pb-3), damit Farben und
  Linienstärke auf kleineren Screens nicht mehr unten rauslaufen

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 22:34:30 +02:00
Pepe Ziberi
3a5c7419f9 fix(ux): Gefahrenzone-Werkzeug + Linienstärke + Text-Reset (v1.6.6)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 33m35s
Aus interner UX-Review:
- Gefahrenzone-Werkzeug (Shortcut D) fehlte komplett in der Werkzeugleiste →
  am Handy (ohne Tastatur) gar nicht auslösbar. Jetzt mit AlertTriangle-Icon drin.
- Linienstärke-Regler ergänzt (Dünn/Mittel/Dick) — die Props selectedWidth/
  onWidthChange waren vorhanden, aber nirgends bedienbar.
- Text-Dialog setzt sich beim Öffnen zurück (kein alter Text nach Abbrechen mehr).
- Einsatz-Beschreibung als mehrzeiliges Textfeld statt einzeiligem Input.

Hinweis: Audit-Trail-Trigger bewusst NICHT wieder ergänzt — war auf ausdrücklichen
Wunsch aus dem Menü entfernt worden.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:41:20 +02:00
Pepe Ziberi
92e1e65b71 fix(journal/rapport): SOMA atomar + Rapport-Korrektheit + Export-dpr (v1.6.5)
- SOMA-Init atomar: pg_advisory_xact_lock pro Projekt in Transaktion +
  createMany → keine doppelten SOMA-Listen mehr, wenn zwei Geräte denselben
  frischen Einsatz gleichzeitig öffnen. initCheckItems meldet Änderung an
  andere Clients (journal-refresh).
- Rapport: korrigierte (durchgestrichene) Original-Einträge werden nicht mehr
  als gültige Massnahmen übernommen; Erledigt-Status ("— erledigt HH:MM")
  erscheint jetzt im Rapport; SOMA zeigt okAt, falls confirmedAt fehlt.
- Export: dpr-Fallback auf devicePixelRatio, wenn die Karte ausgeblendet ist
  (Journal-Tab) — vorher Infinity → NaN-Grössen, Export defekt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:38:16 +02:00
Pepe Ziberi
0e65721a1c fix(sync): Editier-Lock-Versionierung + Zeichnen-Race + Mess-Race (v1.6.4)
- handleStopEditing speichert jetzt MIT baseVersion und aktualisiert
  featuresVersionRef aus der Antwort (409 → Server-Stand übernehmen). Verhindert
  Fehl-Konflikte/Konflikt-Schleifen beim erneuten Bearbeiten nach dem Freigeben.
- Lock-Ablehnung (zwei zeichnen bei freier Karte gleichzeitig): der abgelehnte
  Client lädt den echten Sperr-Halter nach → Karte wird lokal read-only, keine
  weiteren nicht-speicherbaren Änderungen mehr.
- Mess-Höhenabfrage: Generation-Zähler bricht veraltete asynchrone Abfragen ab
  (Neue Messung/Verlassen) → keine falschen Höhen-/Druckwerte der alten Messung.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:36:00 +02:00
Pepe Ziberi
d2a1e7fc1e fix(security): Mandanten-Isolation + Rechte-Ausweitung geschlossen (v1.6.3)
Aus interner Sicherheits-Review (verifiziert):

- Rapporte (GET+POST /api/rapports): Tenant-Check via getProjectWithTenantCheck
  ergänzt — vorher konnte jeder eingeloggte Nutzer mit fremder projectId
  Rapporte fremder Mandanten auflisten (inkl. Token → voller Inhalt) bzw.
  anlegen. POST blockt jetzt zusätzlich VIEWER.
- Plan-Bild (GET .../plan-image/serve): globaler Lookup → getProjectWithTenantCheck.
- Journal-Suggestions (GET /api/tenants/[tenantId]/suggestions): Membership-Check
  ergänzt (kein Cross-Tenant-Lesen mehr).
- Admin-User-Anlegen: Nicht-SERVER_ADMIN kann nur noch im EIGENEN Tenant anlegen
  (mitgeschickte tenantId aus dem Body wird ignoriert) — verhindert Rechte-
  Ausweitung in fremde Organisationen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:32:33 +02:00
Pepe Ziberi
d758c8c744 feat(wind): taktische Live-Windanzeige in der Kopfzeile (v1.6.2)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 32m13s
- Wind-Badge oben in der Topbar: Pfeil zeigt die Ausbreitungsrichtung
  (wohin es treibt, Nord oben) + Himmelsrichtung „aus NW", mit Live-Punkt
- Aktualisiert sich in Echtzeit: solange im Live-Modus, alle 10 Min. frische
  Windrichtung von Open-Meteo für den Einsatzort
- Manuelles Setzen (Windrose) schaltet Live-Modus ab; „Live vom Standort"
  schaltet ihn wieder an
- Tooltip: „Wind aus NW (315°) — treibt nach SO · live"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:11:42 +02:00
Pepe Ziberi
997e200d68 feat(wind): Live-Windrichtung vom Standort (Open-Meteo) (v1.6.1)
- Neuer „Live vom Standort"-Knopf in der Windrose holt die aktuelle
  Windrichtung + Geschwindigkeit von Open-Meteo (kostenlos, ohne Key)
- Beim Öffnen eines Einsatzes ohne gesetzten Wind wird die Windrichtung
  automatisch vom Einsatzort vorgeschlagen (einmal pro Projekt, online)
- Toast zeigt Richtung, Grad, km/h und Böen
- Neue lib/wind.ts (fetchLiveWind)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:01:24 +02:00
Pepe Ziberi
6b6bae9ce1 feat(krokier): Nordpfeil + Windrichtung + Export-Dokument (v1.6.0)
Macht aus dem digitalen Kroki ein echtes taktisches Lagebild:

Karte:
- Nordpfeil-Overlay (dreht mit dem Karten-Bearing mit)
- Windrichtung in 8 Richtungen setzbar (Windrose mit Drift-Pfeil + Label
  "Wind aus NW"), pro Einsatz gespeichert (Project.windDirection)

Export (PNG & PDF):
- Nordpfeil, Massstabsbalken und Windanzeige werden in die Karte eingebrannt
- PDF zusätzlich: Symbol-Legende (Namen aus /api/icons) + bestehende Kopfzeile
  (Einsatz/Ort/Zeit/Nr) → fertiges, weitergebbares Krokier-Dokument

Technik:
- windDirection: Schema + idempotente Migration + Zod-Validierung + Typ
- Neue Komponente MapCompass (Nordpfeil/Windrose/Wind-Setzer)
- MapView verfolgt Bearing über 'rotate'-Event

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:56:11 +02:00
Pepe Ziberi
226a15e882 fix(measure): Messlinie verschwand nach dem Abschliessen (v1.5.9)
Ursache: finishMeasure schaltete auf 'select' um → der Reset-Effekt
(drawMode !== 'measure') löschte die eben gezeichnete Linie sofort; die
Höhen-Labels kamen asynchron danach und blieben, die Linie nicht.

- Nach dem Abschliessen bleibt der Mess-Modus aktiv → Linie + Labels + Panel
  bleiben stehen (Tippen fügt keinen Punkt mehr hinzu, da bereits fertig)
- Gemeinsamer clearMeasurement-Helfer: Panel-X und „Neue Messung" räumen jetzt
  Linie, Marker, Höhen-Labels und Panel vollständig auf
- Beenden weiterhin über „Neue Messung" oder Wahl eines anderen Werkzeugs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:42:23 +02:00
Pepe Ziberi
e9e8f3fa8e fix(tour): Anker repariert + robuste Hervorhebung (v1.5.8)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 33m4s
Nach dem Topbar-Umbau zeigte die Tour auf nicht (mehr) vorhandene bzw.
grössenlose Elemente:
- data-tour="toolbar" lag auf einem display:contents-Div (0×0) → jetzt auf der
  echten Werkzeug-<aside>
- data-tour="sidebar" wurde als Prop an RightSidebar gegeben, aber nie ins DOM
  durchgereicht → jetzt direkt auf der Sidebar-<aside>
- updateHighlight ignoriert fehlende/unsichtbare/off-canvas Ziele (z.B. Sidebar
  auf dem Handy) und zeigt den Tooltip zentriert statt einer kaputten Box
- Tooltip responsiv (max-w statt fix 340px) → kein Überlauf auf schmalen Handys

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:02:19 +02:00
Pepe Ziberi
a4d0a10794 harden(modules): Payload-Grenze + Modul-ID-Prüfung nach Sicherheits-Review (v1.5.7)
Unabhängige Security-Review bestätigte: keine Autorisierungs-/Mandanten-Lücken
(Tenant-Isolation via getProjectWithTenantCheck, VIEWER-Schreibsperre, keine
Injection). Zwei Härtungspunkte umgesetzt:

- module_items data-Payload auf 20 KB pro Zeile begrenzt (POST+PUT) → kein
  DB-Bloat/DoS durch beliebig grosse JSON-Objekte
- moduleId-Länge auf 64 Zeichen begrenzt (verhindert pathologische Keys)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:55:01 +02:00
Pepe Ziberi
e02ea21c93 feat(exercise): Übungs-Cockpit nutzt den Modul-Baukasten + Testplan (v1.5.6)
- Übungen zeigen jetzt zusätzlich zu Zielen/Auswertung dieselben eigenen
  Tabellen-Module wie Einsätze (z.B. Atemschutz, Kräfte vor Ort)
- Live-Sync der Modul-Zeilen auch in der Übung (journal-updated Socket)
- docs/TESTPLAN.md: strukturierter Handy-/Einsatz-Testplan

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:53:37 +02:00
Pepe Ziberi
0a6c9cbcf2 feat(rapport): eigene Cockpit-Module erscheinen im Einsatzrapport (v1.5.5)
- Rapport öffnen sammelt jetzt auch die Daten der eigenen Tabellen-Module
  (Atemschutz, Kräfte vor Ort, Lagemeldungen, eigene Checklisten)
- Neue Rapport-Sektion rendert jede Modul-Tabelle mit ihren Spalten
  (Text / Haken / Zeit) im PDF unter Pendenzen
- Vorschau der Module auch im Rapport-Dialog
- Zeit-Spalten: ISO → HH:MM, Haken → ✓/—

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:50:02 +02:00
Pepe Ziberi
bbf6fbdd13 feat(field): Symbole am Stück setzen + eigene GPS-Position (v1.5.4)
- Tap-to-place bleibt scharf: mehrere gleiche Symbole nacheinander setzen
  (z.B. 4 Hydranten), Hinweis zeigt Anzahl + "Fertig"-Knopf
- Zeichen-Werkzeug wählen beendet den Platzier-Modus
- GPS: eigene Position dauerhaft mit Genauigkeitskreis + Blickrichtung
- Beim Öffnen bleibt die Karte auf dem Einsatzort (springt nicht mehr auf
  die eigene Position, wenn ein Einsatzort gesetzt ist)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:46:43 +02:00
Pepe Ziberi
03914083df refactor(topbar): professionelle einzeilige Kopfzeile (v1.5.3)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 31m6s
Kopfzeile war überladen (~11 konkurrierende Buttons in rot/blau/amber) und die
Versionsnummer kaum lesbar. Neu wie ein Einsatz-Cockpit — ruhig, dreizonig:

- Zone 1 Identität: Logo + "LAGEPLAN" + gut lesbare Version (mono)
- Zone 2 Blickfang: aktiver Einsatz als Pille (Status-Punkt rot=Einsatz /
  blau=Übung, Einsatz-Nr mono, Titel, Ort) — mittig, flex-1, statt xl-only-Box
- Zone 3: grosse Uhr + Speichern + "+ Neu ▾" (Split-Button Einsatz/Übung) +
  Werkzeug-Menü (Export, Präsentation, Vollbild, Tag/Nacht, Einsätze
  verwalten, Schlaucheinstellungen) + Beenden + Benutzer

- Nur noch EIN Farbakzent (Status-Punkt) statt rot+blau+amber-Buttons
- 4 einzelne Icon-Buttons (Theme/Vollbild/Präsentation) → ein Zahnrad-Menü
- Zwei farbige "Neuer Einsatz/Neue Übung"-Buttons → ein "Neu"-Split-Button
- Mobile-Menü entschlackt (Erstellen liegt jetzt im Neu-Button)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 20:10:36 +02:00
Pepe Ziberi
668476948e fix(mobile): Symbol-Tap auf dem Handy zuverlässig – TouchBackend schluckte onClick (v1.5.2)
Ursache: react-dnd TouchBackend (delayTouchStart:150) unterdrückt auf Touch-
Geräten den onClick der Drag-Quelle → Tap-to-place löste auf dem Handy nie aus,
und echtes Touch-Drag über die MapLibre-Karte ist ohnehin unzuverlässig.

- DraggableSymbol erkennt Taps jetzt über echte Touch-Events (onTouchStart/End):
  kurzer, ortsfester Tipp (<12px, <600ms) = Auswahl → Karte antippen platziert
- preventDefault verhindert Doppel-Auslösung durch den synthetischen Klick
- onClick bleibt für Desktop/Maus erhalten

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 18:59:54 +02:00
Pepe Ziberi
796ce53db3 feat(offline): Offline-Karten – Einsatzgebiet wird automatisch vorgeladen (v1.5.1)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 30m48s
Der härteste Einsatztauglich-Punkt: bisher war die Karte ohne Empfang nur dort
verfügbar, wo man schon einmal hingezoomt hatte. Jetzt:

- Beim Öffnen eines Einsatzes werden die OSM-Kacheln des Einsatzgebiets im
  Hintergrund vorgeladen (Zoom 13-18, ~800m Radius, max. 400 Kacheln)
- Exakt das MapLibre-Subdomain-Schema (x+y)%3 → Cache-Keys stimmen überein
- Bereits gecachte Kacheln werden übersprungen (schont OSM-Server + Akku)
- Parallelität 6, Start 4s nach Projektladen (Karte rendert zuerst)
- Marker pro Einsatz in localStorage, Auffrischung nach 14 Tagen
- Toast "Karte offline verfügbar" nach Abschluss
- Neue Bibliothek src/lib/offline-tiles.ts (Web-Mercator-Kachelmathematik)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 18:19:19 +02:00
Pepe Ziberi
2dbb2204f6 feat(cockpit): Modul-Baukasten – Einsatz-Cockpit frei zusammenstellbar (v1.5.0)
Jede Feuerwehr stellt ihr Cockpit selbst zusammen (Wunsch: modular & einfach):

M0 – Zerlegt:
- SOMA + Pendenzen als eigenständige Modul-Komponenten, gemeinsame Typen
  (journal/types.ts), einheitliche ModuleCard-Hülle

M1 – Registry & Verwaltung:
- Modul-Registry src/lib/modules.ts, Konfiguration pro Mandant in
  tenants.modulesConfig (JSONB, idempotente Migration)
- API GET/PUT /api/tenant/modules (normalisiert + validiert via normalizeModules)
- "Module"-Button im Journal (Admins): an/aus, Reihenfolge hoch/runter,
  eigene Module löschen

M2 – Eigene Module:
- Generisches Tabellen-Modul: frei definierbare Spalten (Text / Haken mit
  Zeitstempel / Auto-Zeit), max 8 Spalten
- Generische Daten-API /api/projects/[id]/modules/[moduleId]/items (+ [itemId]),
  neue Tabelle module_items, Live-Sync über journal-refresh Events

M3 – Feld-Vorlagen:
- Checkliste, Atemschutz-Überwachung (Trupp/Druck/Zeit/Draussen),
  Kräfte vor Ort (Name/Funktion/seit/Weg), Lagemeldungen (Hauptbereich),
  leeres Modul mit Spalten-Editor

Eingebaute Module (Journal/SOMA/Pendenzen) bleiben ohne Konfiguration exakt
wie bisher — kaputte Configs fallen sicher auf den Standard zurück.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 18:15:34 +02:00
Pepe Ziberi
6eab656ae6 fix(review): App-Kontrolle – 3 Funde behoben (v1.4.17)
Komplett-Check nach den UX-Umbauten (Typecheck + Produktions-Build + Code-Review):

- Tastenkürzel (V/L/G/...) lösen jetzt ebenfalls die Auto-Bearbeitung aus —
  vorher umgingen sie ensureEditing und der erste Strich wurde nicht gesynct
- ensureEditing feuert nicht mehr mehrfach parallel (editingLoading-Guard)
- Onboarding-Tour aktualisiert: Schritt "Bearbeitung starten" (Button existiert
  nicht mehr) → "Einfach loszeichnen"; Symbol-Schritt erwähnt Tap-to-place

Geprüft, ohne Befund: Auto-Save-Gating, Status-Zeile, flache Sidebar,
Linientyp-Dialog inkl. Gefahrenzone, Demo-Seite, Produktions-Build (Exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:58:01 +02:00
Pepe Ziberi
b1451b665a feat(ux): 3-fach entwirrt – Auto-Bearbeitung, Status-Zeile, flache Symbol-Sidebar (v1.4.16)
Auto-Bearbeitung (Lock vereinfacht):
- Kein manuelles "Bearbeitung starten" mehr: beim Werkzeugwählen/Zeichnen/Symbol-
  Antippen wird der Editier-Lock still übernommen (ensureEditing), sofern die Karte
  frei ist. Lock/Konflikt-Anzeige erscheint nur noch bei echtem Mehrpersonen-Zugriff.
- handleStartEditing bekommt silent-Option + boolean-Rückgabe.
- "Karte freigeben" bleibt als kleine Aktion für Übergabe.

Banner → eine kompakte Status-Zeile:
- Übung/Offline/E-Mail/Bearbeitung nicht mehr bis zu 4 Zeilen gestapelt, sondern
  eine einzige Chip-Zeile, die nur zeigt was zutrifft (sonst gar nichts).

Symbol-Sidebar flacher:
- Statt zwei verschachtelter Klapp-Sektionen (Meine Symbole/Bibliothek) mit Unter-
  Kategorie-Tabs jetzt EINE Chip-Reihe (Mandant + Bibliothek) + ein Raster.
- Suche zeigt Treffer flach über alle Kategorien.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:52:17 +02:00
Pepe Ziberi
efedbdba4d refactor(topbar): Handy-Menü entwirrt – ein einziges Menü statt zwei (v1.4.15)
- Mobil nur noch EIN ⋮-Menü (früher ☰ links + ⋮ rechts → verwirrend)
- Topbar mobil reduziert auf: Logo · Speichern · Neuer Einsatz · Menü
- Selten gebrauchte Buttons (Neue Übung, Präsentationsmodus) wandern ins Menü
- Menü gruppiert mit Trennern: Einsatz/Übung · Export/Karte · Ansicht · System/Konto
- Voller Funktionsumfang mobil erreichbar (Einsätze verwalten, Export, Admin,
  Schlaucheinstellungen, Tour, Kennwort, Abmelden)
- Desktop-Layout unverändert

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:41:52 +02:00
Pepe Ziberi
2556a5c4a4 feat(mobile+ux): Vollbild-Button auf Karte + Standort-Vorschlag beim Einsatz (v1.4.14)
- Mobile Vollbild-Button schwebend oben links auf der Karte (versteckt Browser-Leiste,
  document.fullscreen); Icon wechselt zwischen Vollbild/Verlassen
- Neuer Einsatz: aktueller Standort wird beim Öffnen automatisch als Einsatzort
  vorgeschlagen (Reverse-Geocoding via Nominatim), plus "Mein Standort"-Button
- Setzt zugleich mapCenter → Karte startet direkt am Standort
- Bei verweigertem/fehlendem Standort klarer Hinweis, manuelle Eingabe bleibt möglich
- Adress-Anzeige in gemeinsamen buildDisplayName() ausgelagert

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:33:58 +02:00
Pepe Ziberi
703265f47e feat(draw): Linien-Typ-Abfrage – Normal / Leitung / Rettungsachse (v1.4.13)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 31m14s
- Beim Zeichnen von Linie/Pfeil fragt der Dialog jetzt den Linientyp ab:
  Normal (gewählte Farbe), Leitung (blau, dicker), Rettungsachse (grün, gestrichelt)
- Typ in properties.lineType gespeichert; Leitung/Rettungsachse setzen konventionelle
  Farbe + Stärke, Normal behält die Zeichenfarbe
- Eigener MapLibre-Layer mit Dash-Muster für Rettungsachse (klar als freizuhaltende Achse erkennbar)
- Typ-Auswahl auch ohne Label übernehmbar; Änderung wird live gesynct
- ROADMAP: 5.1 abgehakt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 15:27:42 +02:00
Pepe Ziberi
6e3663cf7f feat(mobile): Handy-Optimierung – Tap-to-place, Bottom-Nav, kompakte Banner (v1.4.12)
- Tap-to-place: Symbol antippen → auf Karte tippen platziert es (Alternative zu Drag&Drop, mobil-tauglich); schwebender Hinweis mit Abbrechen
- Mobile Bottom-Navigation: Karte / Journal(Auswertung) / Symbole ohne Menü erreichbar
- FAB entfernt; Karten-Steuerung (Zoom/Standort/Massstab) hebt sich mobil über die Bottom-Nav
- Touch-Ziele in der Werkzeugleiste auf 44px vergrössert
- "Einsatz/Übung beenden" jetzt auch im Mobile-Overflow-Menü
- Banner (Übung/Offline/E-Mail/Live) mobil kompakt: kleinere Höhe, gekürzte Texte, truncate
- Einsatz-beenden-Bestätigung als In-App-Dialog statt Browser-confirm()

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 15:20:27 +02:00
Pepe Ziberi
947a757d9b feat(robustheit): optimistische Nebenläufigkeit (featuresVersion + Konflikt-Erkennung)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 28m41s
Verhindert stilles Überschreiben neuerer Änderungen durch veralteten Stand
(z.B. nach Offline-Reconnect oder Lock-Ablauf bei Multi-Device).

- Project.featuresVersion (Schema + Auto-Migration), pro Features-Speichern +1
- Features-PUT: optimistischer Lock in interaktiver Transaktion
  (updateMany WHERE featuresVersion=baseVersion -> atomar; count=0 => 409 Konflikt)
- Bei Konflikt liefert der Server den aktuellen Stand zurück (409 + features + version)
- Client sendet baseVersion (Auto-Save, manuell, Beacon, Offline-Queue), übernimmt
  bei 409 den Server-Stand und warnt den Nutzer (statt still zu überschreiben)
- Offline-Queue verwirft konfligierende 4xx (kein Clobber beim Reconnect)
- Transaktions-Timeout 15s für grosse Zeichnungen auf langsamer DB

Version 1.4.11

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 02:07:24 +02:00
Pepe Ziberi
8b5f4a6778 fix(robustheit+ux): atomares Speichern, Offline-Härtung, Einsatz-Beenden, Audit raus
Robustheit / Datensicherheit:
- Features-PUT in prisma.$transaction (delete+create atomar) — verhindert
  Totalverlust bei Abbruch zwischen Löschen und Neuanlegen
- localStorage-Features pro Projekt (lageplan-features-<id>) statt globalem Key
  — verhindert Datenübertrag zwischen Projekten
- Sync-Queue verwirft dauerhafte 4xx-Fehler statt Endlos-Retry
- beforeunload persistiert auch leere Feature-Listen (Löschungen gehen nicht verloren)

UX:
- Leitungsrechner-Dropdown: Popover z-[2000] (lag hinter dem Panel), Label gekürzt
- Neuer "Einsatz beenden"-Button (speichert, löst Lock, schliesst Einsatz)
- Audit-Trail-Einstiege entfernt (Toolbar-Button + Menü-Eintrag)

Version 1.4.10

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 01:38:13 +02:00
Pepe Ziberi
ddb7c63600 refactor(leitungsrechner): professionelles React-Panel + zentrale Berechnung
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 33m10s
- Neue lib src/lib/hose-calc.ts: EINZIGE Quelle der Druckformel (typisiert),
  inkl. Schlauchanzahl aus lengthPerPieceM (Feld war vorher tot) + 10% Verlegereserve
- MeasurePanel als echte React-Komponente ersetzt das innerHTML/Inline-CSS-Overlay
  in map-view.tsx (~130 Zeilen DOM-Gebastel entfernt)
- Schlauchtyp im Messergebnis wählbar (Standard vorausgewählt) statt alle gestapelt
- Strahlrohrdruck einstellbar (4/5/6/8 bar), Ergebnis rechnet live neu
- Höhendaten-Quelle ehrlich ausgewiesen; sichtbare Warnung wenn keine Höhendaten
  (vorher stilles "alles flach")
- hose-settings-dialog nutzt die zentrale Formel (Duplikat entfernt)
- Version 1.4.9

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 00:44:55 +02:00
Pepe Ziberi
bae1cfe9a0 feat(collab): Karte + Journal parallel bearbeitbar
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
- Editor-Lock aufgeteilt: canEditMap (Lock-geschützt) vs. canEditJournal (nur Rolle)
- Eine Person kann zeichnen, während eine andere parallel Journal führt
  (Führungsstab-Szenario: Zeichner + Journalführer)
- Journal-Live-Sync existierte bereits (Socket journal-updated -> journal-refresh)
- Banner-Texte präzisiert ("bearbeitet die Karte", "Journal bleibt bearbeitbar")
- Version 1.4.8

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 00:39:31 +02:00
Pepe Ziberi
a081b3ebe4 refactor(admin): 1365-Zeilen-Seite in saubere Bereichs-Komponenten entwirrt
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 30m12s
- admin/page.tsx: 1365 -> 224 Zeilen (nur noch Guard + Header + Tabs -> Komponenten)
- Neue in sich geschlossene Komponenten (eigener State + Daten-Laden):
  tenants-tab, users-tab, projects-tab, icons-tab, categories-tab
- Gemeinsame Typen/Konstanten in admin-constants.ts zentralisiert
- SettingsTab lädt Zähler selbst (von der Hülle entkoppelt)
- Toten Code entfernt (UpgradeRequestsTab)
- Ergänzt: Benutzer-Suche + Mandanten-Auswahl beim Anlegen (verhindert verwaiste User)
- Version 1.4.7

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 23:31:17 +02:00
Pepe Ziberi
d08b96a42a feat(admin): echtes Super-Admin-Benutzer-Management + letzte Anmeldung
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
- Super-Admin sieht jetzt ALLE Benutzer über alle Mandanten in einer Tabelle
  (statt nur Server-Admins). Neue Spalten: Mandant, Letzte Anmeldung, Status
- Einzelne Benutzer sperren/entsperren: User.isActive (Schema + Auto-Migration),
  Login-Sperre für deaktivierte Konten, PATCH /api/admin/users/[id] { isActive }
- GET /api/admin/users liefert jetzt lastLoginAt + isActive
- Selbstsperre verhindert; admin@lageplan.local geschützt
- Version 1.4.6

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 23:15:16 +02:00
Pepe Ziberi
bae5a36492 perf(ci): Build-Zeit senken – BuildKit-Cache-Mounts + mode=min
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 32m35s
- Dockerfile: --mount=type=cache für /root/.npm (npm ci) und /app/.next/cache
  (inkrementeller next build statt jedes Mal von Null)
- deploy.yml: cache-to mode=max -> mode=min (spart auf dem NAS den ~12 Min
  Cache-Export, der bisher grösster Einzelposten war)
- provenance/sbom aus (weniger Attestation-Export)
- ENV-Legacy-Warnungen behoben (ENV key=value)
- Version 1.4.5

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 22:30:42 +02:00
Pepe Ziberi
1c8f619252 feat(exercise): Übungsmodus mit Übungs-Cockpit + direkte Einstiegs-Buttons
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 40m38s
- Topbar: direkte Buttons "Neuer Einsatz" (rot) und "Neue Übung" (blau)
  statt versteckt im Menü; öffnen den Dialog direkt im richtigen Modus
- Übungs-Cockpit ersetzt bei Übungen das Journal: Übungsziele mit
  Zielerreichung (Offen/Erreicht/Teilweise/Nicht erreicht) + Auswertung
- Neues Model ExerciseGoal + Project.exerciseEvaluation (Auto-Migration)
- API-Routen /projects/[id]/exercise-goals (Liste/Erstellen/Update/Löschen)
- Deutliches "ÜBUNG"-Banner gegen Verwechslung mit echtem Einsatz
- Sidebar-Tab heisst bei Übung "Auswertung"
- Version 1.4.4

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 21:16:32 +02:00
Pepe Ziberi
1641101b73 feat(projects): Einsatz/Übung-Modus
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 22m24s
- Feld mode (EINSATZ/UEBUNG) am Projekt + idempotente Auto-Migration
- Auswahl beim Erstellen (Segmented-Control mit Lucide-Icons)
- Badge "Einsatz"/"Übung" in der Projektliste
- Eigene Nummerierung für Übungen (Ü-YYYY-NNNN, getrennt gezählt)
- Journal-Titel wird bei Übung zu "Übungs-Journal"
- ROADMAP.md mit Verbesserungs-Fahrplan ergänzt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 20:12:07 +02:00
Pepe Ziberi
93af663dd1 fix(auth): JWT-Secret härten und Session-Laufzeit verkürzen
- Gemeinsames Secret-Modul (jwt-secret.ts): bricht in Produktion hart ab,
  wenn NEXTAUTH_SECRET fehlt oder <32 Zeichen – kein öffentlicher Fallback mehr
- auth.ts und middleware.ts nutzen dieselbe Secret-Quelle (kein Divergieren)
- Schwacher Compose-Fallback entfernt: Deployment ohne Secret bricht ab (:?)
- "Angemeldet bleiben" 30 → 14 Tage; Laufzeiten zentralisiert (SESSION_MAX_AGE_*)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 20:11:56 +02:00
Pepe Ziberi
08ecf4c61a fix(schema): ensureTables() fügt jetzt fehlende Spalten nachträglich hinzu – verhindert altes Schema-Problem
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 11m16s
2026-05-21 15:42:22 +02:00
Pepe Ziberi
6588959b50 fix(tenant-symbols): isActive Filter auf NULL tolerant – zeigt Symbole auch ohne isActive-Wert
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2026-05-21 15:36:13 +02:00
Pepe Ziberi
a91a4c4689 fix(migrate): customName + sortOrder Spalten zu tenant_symbols hinzugefügt
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2026-05-21 15:34:11 +02:00
Pepe Ziberi
693515aab5 fix(tenant-symbols): POST /api/tenant/symbols iconAsset-Legacy-Pfad auf Raw-SQL umgestellt
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2026-05-21 15:26:14 +02:00
Pepe Ziberi
0cbea843ab fix(symbol-groups): API + Frontend auf category-Objekt-Format umgestellt – zeigt jetzt alle Kategorien korrekt
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2026-05-21 15:25:12 +02:00
Pepe Ziberi
9cba24aad8 fix(icons): /api/icons auf Raw-SQL umgestellt – zeigt jetzt alle Tenant-Kategorien in der App
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 11m39s
2026-05-21 15:00:47 +02:00
Pepe Ziberi
93d5519e58 fix(tenant-symbols): kompletter Endpoint auf Raw-SQL umgestellt
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2026-05-21 14:58:19 +02:00
Pepe Ziberi
c291431fd7 fix(categories): kompletter Endpoint auf Raw-SQL umgestellt – unabhängig von Prisma-Client-Modellen
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 10m51s
2026-05-21 14:31:54 +02:00
Pepe Ziberi
3606c9a2a4 fix(auto-migrate): tenant_categories on-the-fly erstellung + migrate.js fail-fast
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 10m41s
2026-05-21 13:40:34 +02:00
Pepe Ziberi
3722a04091 fix(categories): POST/PATCH/DELETE mit Fallback bei fehlendem Schema
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 11m44s
2026-05-21 08:51:54 +02:00
Pepe Ziberi
0d0d9a7257 fix(symbol-manager): Select.Item empty string replaced with '__none__' placeholder
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 14m56s
2026-05-21 08:16:20 +02:00
Pepe Ziberi
40cea9a9be refactor(symbol-manager): remove template import, focus on library UX
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 12m43s
2026-05-21 07:41:17 +02:00
Pepe Ziberi
56895be16f fix(symbol-arch): robuste API-Endpoints + docker-entrypoint Migration/Seed
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 14m59s
2026-05-21 07:05:49 +02:00
Pepe Ziberi
e9f66b2c3d feat(1.4.3): add 'Bibliothek' tab to admin for single-icon selection
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 20m6s
2026-05-20 23:36:00 +02:00
Pepe Ziberi
c8a94e1ea7 hotfix(1.4.2): admin empty + legacy symbol migration
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 20m44s
2026-05-20 23:06:25 +02:00
Pepe Ziberi
07cede68c0 hotfix(1.4.1): fix migration FK failure + null toLowerCase crash
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 18m13s
2026-05-20 22:08:40 +02:00
Pepe Ziberi
f6819b6a2b Release 1.4.0: Phase 1 Symbol-Architektur Redesign
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 20m43s
2026-05-20 21:44:07 +02:00
Pepe Ziberi
cfccd4cdcc Phase 1: Dokumentation und Plan aktualisiert 2026-05-20 21:33:53 +02:00
Pepe Ziberi
4602de7a38 Phase 1 Sprint C+D: Admin UI + Frontend Sidebar 2026-05-20 21:29:45 +02:00
Pepe Ziberi
ca26f1e733 Phase 1 Sprint B: Neue Tenant-Symbol APIs 2026-05-20 21:19:17 +02:00
Pepe Ziberi
3b57ca4594 docs(roadmap): Phase 1.1 Tasks aufgeteilt in erledigt/offen
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 13m43s
2026-05-20 21:09:53 +02:00
Pepe Ziberi
210ef9d99e docs(roadmap): Phase 1 Status aktualisiert — Sprint A erledigt
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 12m53s
2026-05-20 20:56:00 +02:00
Pepe Ziberi
08a66095e6 chore(seed/docs): production safety, scripts audit, documentation
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 17m48s
2026-05-20 17:32:28 +02:00
Pepe Ziberi
ecc9003a73 fix(seed/docker): repair production symbol loss incident
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 21m31s
2026-05-20 16:56:22 +02:00
Pepe Ziberi
a53f77c97c fix(db): comprehensive symbol recovery + safety fixes
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 22m1s
2026-05-20 15:05:44 +02:00
Pepe Ziberi
fdd928720a docs: add incident report for symbol loss + recovery script 2026-05-20 13:58:37 +02:00
Pepe Ziberi
5adadd246e fix(seed): prevent cascade-deletion of tenant_symbols on container restart
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 12m23s
2026-05-20 11:44:12 +02:00
Pepe Ziberi
4b92df8fea feat(schema): Phase 1 Symbol Architecture — SymbolTemplate, TenantCategory, TenantSymbol refactor + seed + migration scripts
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 14m40s
2026-05-20 11:11:32 +02:00
Pepe Ziberi
9b96de0a21 feat: Präsentationsmodus (Schloss-Button) + Version 1.3.5
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 16m59s
2026-05-20 08:35:52 +02:00
Pepe Ziberi
902e730cd3 trigger: force Gitea Actions run
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 11m20s
2026-05-19 23:20:55 +02:00
Pepe Ziberi
1f891ab057 fix: track Pepe_Avatar.mp4 and allow public/*.mp4 in git 2026-05-19 23:19:57 +02:00
Pepe Ziberi
165109fc65 fix(docker): copy public folder after standalone to ensure assets are present
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 8m51s
2026-05-19 22:45:18 +02:00
Pepe Ziberi
b6fbe38f60 fix(docker): add missing Next.js runtime deps to runner image
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 10m11s
2026-05-19 22:06:09 +02:00
Pepe Ziberi
053ae3729a fix(ci): revert REGISTRY_PASSWORD back to REGISTRY_TOKEN
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 20m23s
2026-05-19 21:16:54 +02:00
Pepe Ziberi
8207366362 chore(release): bump version to 1.3.4
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m26s
2026-05-19 21:03:06 +02:00
Pepe Ziberi
29217e883b refactor(ci): switch to Watchtower-only auto-update, remove Portainer webhook 2026-05-19 20:26:43 +02:00
Pepe Ziberi
0f635033c2 feat(ci): trigger Portainer webhook after successful image push 2026-05-19 20:12:16 +02:00
Pepe Ziberi
805559efc3 perf(ci): add Docker registry cache to speed up builds 2026-05-19 20:07:37 +02:00
Pepe Ziberi
5d46200905 chore(release): bump version to 1.3.3
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 14m19s
2026-05-19 19:54:14 +02:00
Pepe Ziberi
5c353a0da8 fix(ci): remove GitHub Actions cache from Gitea workflow
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 15m4s
2026-05-19 19:21:35 +02:00
Pepe Ziberi
ba6f095dc0 perf(docker): optimize runner stage build time
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 15m0s
2026-05-19 18:59:03 +02:00
Pepe Ziberi
362a7e4666 chore(ci): setup Gitea Actions + Portainer auto-deploy pipeline
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2026-05-19 18:07:56 +02:00
Pepe Ziberi
63a57dcb7c ci: switch to Portainer auto-build (like mein118.ch) 2026-04-26 19:40:57 +02:00
Pepe Ziberi
62a5a56dea ci: update deploy workflow for existing runner
Some checks failed
Build and Deploy / deploy (push) Has been cancelled
2026-04-26 19:27:35 +02:00
Pepe Ziberi
eb8566423f ci: add action for portainer deployment
Some checks failed
Build and Deploy to Portainer / build-and-deploy (push) Has been cancelled
2026-03-29 07:00:07 +02:00
Pepe Ziberi
1f508bca74 v1.3.2: SEO fixes + map bugfixes
SEO:
- Landing page converted to Server Component (SSR)
- Extracted NavAuthButtons + ContactForm as client islands
- Removed fake aggregateRating from JSON-LD
- Added FAQPage JSON-LD schema (7 questions)
- Extended sitemap: /datenschutz, /spenden, /demo

Map fixes:
- WebGL context lost recovery (black tiles after inactivity)
- Page visibility handler for tile reload on tab switch
- Arrow direction: geographic bearing instead of screen angle
- All markers rotationAlignment viewport->map (geographic orientation)
- DEL key now deletes selected lines/polygons/arrows (not just symbols)
- Default drawing color: black
2026-03-03 23:33:04 +01:00
Pepe Ziberi
708bdf6be0 v1.3.1: Fix symbol loading, DEL key, SOMA/Pendenzen in rapport, improved onboarding, org settings tab, logo upload 2026-02-25 22:28:10 +01:00
Pepe Ziberi
5917fa88ad v1.3.0: Refactoring Phase 3+4, Symbol-Verwaltung Redesign, Schlauch-Labels Fix
- Refactoring: Error Boundaries, apiFetch Wrapper, Socket Status-Tracking
- Refactoring: UI Kontrast (theme-aware colors), unused imports bereinigt
- Symbol-Verwaltung: Neues Split-Panel (Meine Symbole + Bibliothek)
- Symbol-Verwaltung: Umbenennen (TLF rot/blau), Duplikate erlaubt
- Symbol-Verwaltung: Karten-Sidebar zeigt eigene Symbole bevorzugt
- Schlauch-Labels: Groessere Schrift (13px/10px), verschiebbar (Drag)
- Schema: TenantSymbol customName, sortOrder, unique constraint entfernt
- Open Source Referenz entfernt (kostenloses Projekt)
2026-02-25 00:06:39 +01:00
Pepe Ziberi
8ddeb7b377 v1.2.2: Fix Nominatim CSP, Tenant Admin kann eigene Symbole hochladen 2026-02-24 22:43:05 +01:00
Pepe Ziberi
f480905bb9 hotfix: fetchData erst nach user.role laden (Mandanten verschwunden) 2026-02-24 22:17:12 +01:00
Pepe Ziberi
18398e559c v1.2.1: Fix Ctrl+Z/Y vertauscht, 401-Fehler TENANT_ADMIN, Symbole groesser, Spenden-Tab verbessert 2026-02-24 21:51:23 +01:00
Pepe Ziberi
1583ef2a17 v1.2.0: Symbol-Verwaltung, SOMA-Admin, Light Mode Farbsystem, Onboarding-Tour, Credit-Link 2026-02-24 21:13:27 +01:00
305 changed files with 27169 additions and 4801 deletions

View File

@@ -1,7 +1,7 @@
node_modules
.next
.git
Signaturen
/Signaturen/
*.tar
*.pdf
*.py

13
.env.docker Normal file
View File

@@ -0,0 +1,13 @@
# Dummy environment for Docker build stage
# These values are only needed so Next.js can compile during docker build
# Runtime values are injected via docker-compose environment
DATABASE_URL=postgresql://lageplan:lageplan_secret@db:5432/lageplan
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=build-time-secret-not-used-at-runtime
MINIO_ENDPOINT=minio
MINIO_PORT=9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin123
MINIO_BUCKET=lageplan-icons
MINIO_USE_SSL=false
MINIO_PUBLIC_URL=http://localhost:9000

View File

@@ -27,3 +27,8 @@ MINIO_PUBLIC_URL=http://localhost:9002
# Web App
WEB_PORT=3000
NODE_ENV=development
# --- CI/CD / Registry (nur für Portainer Deployment) ---
# Gitea Registry Login für Watchtower (automatische Image-Updates)
GITEA_REGISTRY_USER=adminpepe
GITEA_REGISTRY_PASS=dein_gitea_token_oder_passwort

View File

@@ -0,0 +1,55 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
- master
workflow_dispatch:
env:
REGISTRY: git.purepixel.ch
IMAGE: git.purepixel.ch/adminpepe/lageplan
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
tags: |
type=raw,value=latest
type=sha,prefix=,suffix=,format=short
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# provenance/sbom aus: spart zusätzlichen Attestation-Export
provenance: false
sbom: false
cache-from: type=registry,ref=${{ env.IMAGE }}:cache
# mode=min statt max: exportiert nur die finalen Schichten → auf langsamem
# NAS-Speicher spart das ~12 Min Cache-Export. Der teure Rechenaufwand
# (npm ci, next build) wird ohnehin über BuildKit-Cache-Mounts abgedeckt.
cache-to: type=registry,ref=${{ env.IMAGE }}:cache,mode=min

12
.gitignore vendored
View File

@@ -49,13 +49,19 @@ prisma/migrations/*
# Large binary files (do not commit)
*.tar
*.zip
*.mp4
# Allow MP4s in public folder (used by the app)
!public/*.mp4
!public/**/*.mp4
lageplan-web.tar
# Reference materials (keep locally, not in git)
Signaturen/
/Signaturen/
Reglement_*/
# Allow FKS SVG signatures in public folder (used by seed scripts)
!public/signaturen/
!public/signaturen/*.svg
!public/**/*.svg
# Stack env (contains secrets)
stack.env
.env.docker

56
CHANGELOG.md Normal file
View File

@@ -0,0 +1,56 @@
# Changelog
Alle nennenswerten Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
## [1.4.3] 2026-05-20 — Feature: Einzelne Symbole aus Bibliothek wählen
### Neu
- **Admin → Symbol-Manager**: Neuer Tab „Bibliothek“ zeigt alle 117 globalen Symbole gruppiert nach Kategorie. Pro Symbol ein „+“-Button, um es mit einem Klick zu „Meinen Symbolen“ hinzuzufügen (via `POST /api/tenant/symbols` mit `iconId`).
## [1.4.2] 2026-05-20 — Hotfix: Admin leer & Legacy-Symbole
### Behoben
- **Admin Symbol-Manager**: Liest jetzt korrekt `categories` statt `groups` aus der API-Response von `/api/tenant/symbols?grouped=true`.
- **Datenmigration (Step 18)**: Neue Migration in `prisma/migrate.js` migriert bestehende `tenantSymbols` automatisch: setzt fehlende `name`, `svgPath`, `categoryId` (Default-Kategorie „Meine Symbole“) und `migratedFromIconId`. Behebt „Leichen“ mit broken images.
## [1.4.1] 2026-05-20 — Hotfix: Production 500 & Null-Crash
### Behoben
- **Migration `prisma/migrate.js`**: `ALTER TABLE ... ADD COLUMN ... REFERENCES` auf PostgreSQL mit bestehenden Daten führte zu stillen Fehlern. Spalte `categoryId` wird jetzt ohne Inline-REFERENCES angelegt; Foreign-Key wird in separatem idempotenten Schritt (15b) erstellt.
- **Frontend `right-sidebar.tsx`**: `s.name.toLowerCase()` crashte wenn Symbol-Name `null` war. Optionaler Fallback auf leeren String hinzugefügt.
## [1.4.0] 2026-05-20 — Phase 1: Symbol-Architektur Redesign
### Neu
- **SymbolTemplate** — globale, read-only Vorlagen-Pakete aus `public/signaturen/*.svg`
- **TenantCategory** — mandantenspezifische, frei anlegbare Kategorien für Symbole
- **TenantSymbol Refactor** — Symbole sind jetzt vollständig mandantenspezifisch (`name`, `svgPath`, `categoryId`, `isUploaded`)
- Upload-Dialog für eigene SVG/PNG/JPEG-Symbole mit Drag & Drop
- Import-Dialog für Vorlagen-Pakete (z.B. "Feuerwehr Schweiz")
### APIs
- `GET /api/templates` — listet verfügbare Template-Pakete mit Vorschau
- `POST /api/templates/import` — importiert ein Paket als TenantSymbols
- `GET/POST/PATCH/DELETE /api/tenant/categories` — CRUD für Tenant-Kategorien
- `GET/POST/PATCH/DELETE /api/tenant/symbols` — erweitert: Gruppierung, Upload, JSON-Import
- `GET /api/tenant/symbols/[id]/image` — liefert TenantSymbol-Bilder aus MinIO oder `public/`
- `GET /api/icons/[id]/image` — TenantSymbol-First Lookup, dann Legacy-Fallback
- `GET /api/icons` — liefert jetzt auch `tenantSymbols` und `tenantSymbolGroups`
### UI
- **Admin → Symbol-Manager**: komplett neues Layout mit 3 Tabs (Symbole, Kategorien, Import)
- **Sidebar (RightSidebar)**: zeigt Tenant-Symbole jetzt nach Kategorie gruppiert an
### Migration & Seed
- `prisma/migrations/20260520_symbol_architecture/migration.sql`
- `prisma/seed-symbol-templates.ts`
- `prisma/migrate-tenant-symbols.ts`
- Alle Seeds idempotent (`upsert` statt `deleteMany`)
---
## [1.3.5] vor 2026-05-20
### Bestehende Features
- Karten-Zeichenwerkzeuge, Journal, Einsatzrapport, Projekte, Benutzerverwaltung, Mandantenverwaltung

View File

@@ -1,10 +1,12 @@
# syntax=docker/dockerfile:1
# Stage 1: Dependencies
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat openssl
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --ignore-scripts --legacy-peer-deps
# npm-Cache über BuildKit persistent halten (schnellere Re-Installs)
RUN --mount=type=cache,target=/root/.npm npm ci --ignore-scripts --legacy-peer-deps
# Stage 2: Builder
FROM node:20-alpine AS builder
@@ -20,42 +22,50 @@ RUN npx prisma generate
COPY .env.docker .env
# Build Next.js
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
ENV NEXT_TELEMETRY_DISABLED=1
# .next/cache über BuildKit persistent halten → inkrementeller Build statt jedes Mal von Null
RUN --mount=type=cache,target=/app/.next/cache npm run build
# Stage 3: Runner
FROM node:20-alpine AS runner
RUN apk add --no-cache openssl
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.env ./.env
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder /app/node_modules/prisma ./node_modules/prisma
COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma
COPY --from=builder /app/node_modules/.bin/prisma ./node_modules/.bin/prisma
COPY --from=builder /app/node_modules/bcryptjs ./node_modules/bcryptjs
COPY --from=builder /app/node_modules/stripe ./node_modules/stripe
COPY --from=builder /app/package.json ./package.json
RUN npm install --omit=dev --legacy-peer-deps socket.io@4.7.4 @react-pdf/renderer@4.3.2 qrcode@1.5.4 --no-save
COPY server-custom.js ./server-custom.js
COPY docker-entrypoint.sh ./docker-entrypoint.sh
RUN chown -R nextjs:nodejs /app/node_modules
# Fast: only chown the /app directory itself, not recursively
RUN chown nextjs:nodejs /app
USER nextjs
# Install only the unbundled runtime deps needed by the custom server.
# Running as USER nextjs means files are already correctly owned — no slow chown -R needed afterwards.
RUN npm install --omit=dev --legacy-peer-deps socket.io@4.7.4 @react-pdf/renderer@4.3.2 qrcode@1.5.4 --no-save
COPY --chown=nextjs:nodejs --from=builder /app/.next/standalone ./
COPY --chown=nextjs:nodejs --from=builder /app/.next/static ./.next/static
# Ensure all public files (videos, images, etc.) are present in the runtime image
COPY --chown=nextjs:nodejs --from=builder /app/public ./public
COPY --chown=nextjs:nodejs --from=builder /app/.env ./.env
COPY --chown=nextjs:nodejs --from=builder /app/prisma ./prisma
COPY --chown=nextjs:nodejs --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --chown=nextjs:nodejs --from=builder /app/node_modules/prisma ./node_modules/prisma
COPY --chown=nextjs:nodejs --from=builder /app/node_modules/@prisma ./node_modules/@prisma
COPY --chown=nextjs:nodejs --from=builder /app/node_modules/bcryptjs ./node_modules/bcryptjs
COPY --chown=nextjs:nodejs --from=builder /app/node_modules/stripe ./node_modules/stripe
COPY --chown=nextjs:nodejs --from=builder /app/node_modules/next ./node_modules/next
COPY --chown=nextjs:nodejs --from=builder /app/node_modules/react ./node_modules/react
COPY --chown=nextjs:nodejs --from=builder /app/node_modules/react-dom ./node_modules/react-dom
COPY --chown=nextjs:nodejs --from=builder /app/package.json ./package.json
COPY --chown=nextjs:nodejs server-custom.js ./server-custom.js
COPY --chown=nextjs:nodejs docker-entrypoint.sh ./docker-entrypoint.sh
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["sh", "docker-entrypoint.sh"]

View File

@@ -0,0 +1,139 @@
# Vorfall-Bericht: Symbol-Verlust (20. Mai 2026)
## Was ist passiert?
Alle Symbole in der App zeigen seit heute Morgen einen 404-Fehler (broken image icon). Die Symbole selbst sind aus der Datenbank gelöscht. Bereits erstellte Zeichnungen sind noch vorhanden, aber die Symbole darin sind ebenfalls broken.
---
## Chronologie der Ereignisse
### 1. Phase 1 Planung & Schema-Änderung (vor dem Vorfall)
Ich habe den Sprint A von **Phase 1 — Symbol-Architektur Redesign** begonnen. Dabei habe ich das Prisma-Schema erweitert:
- **Neue Tabellen**: `SymbolTemplate`, `TenantCategory`
- **Erweiterte Tabelle**: `TenantSymbol` um neue Spalten (`name`, `svgPath`, `isUploaded`, `categoryId`, etc.)
Diese Änderungen waren ausschließlich schema-seitig — es gab keine Löschoperationen.
### 2. Der eigentliche Bug (Cascade Delete)
Das Problem lag **NICHT** in den Schema-Änderungen, sondern in den bestehenden **Seed-Skripten**, die bei jedem Container-Start laufen:
**Dateien:**
- `prisma/seed.js`
- `prisma/seed-icons-only.js`
**Code (VOR dem Fix):**
```javascript
// Zeile ~73 in seed-icons-only.js
const deleted = await prisma.iconAsset.deleteMany({ where: { isSystem: true } })
console.log(`Deleted ${deleted.count} old system icons`)
```
Dieser Code hat **bei jedem Container-Start** alle System-Icons gelöscht und neu erstellt.
### 3. Warum das alles zerstört hat
Im Prisma-Schema gibt es folgende Relation:
```prisma
model TenantSymbol {
id String @id @default(uuid())
iconId String
icon IconAsset @relation(fields: [iconId], references: [id], onDelete: Cascade)
// ...
}
```
**Die `onDelete: Cascade`-Regel bedeutet:** Wenn ein `IconAsset` gelöscht wird, werden automatisch alle `TenantSymbol`-Einträge, die darauf verweisen, ebenfalls gelöscht.
**Ablauf bei jedem Container-Start:**
1. `seed-icons-only.js` läuft (weil Icon-Zahl < 100)
2. `deleteMany({ isSystem: true })` löscht alle System-Icons
3. `onDelete: Cascade` löscht alle `TenantSymbol`-Einträge
4. Neue Icons werden mit **neuen UUIDs** erstellt
5. Bestehende Zeichnungen verweisen noch auf die **alten gelöschten UUIDs** → 404
### 4. Warum ist das jetzt aufgefallen?
Der Container wurde neu gestartet (neues Deployment), und das Seed-Skript ist gelaufen. Dabei wurden alle Symbole gelöscht. Dieser Bug existierte bereits vor meinen Änderungen in den Seed-Skripten, ist aber jetzt zum ersten Mal aufgetreten, weil:
- Vorher hat das Skript nur selten gegriffen (Icons waren schon genug da)
- Jetzt wurde der Container frisch gestartet, und die Bedingung `ICON_COUNT < 100` war true
---
## Auswirkungen
### Was ist weg?
- ❌ Alle `IconAsset`-Einträge (System-Symbole)
- ❌ Alle `TenantSymbol`-Einträge (mandantenspezifische Symbol-Aktivierungen)
- ❌ Alle Icon-Category-Zuordnungen
### Was ist noch da?
- ✅ Alle Projekte (Zeichnungen)
- ✅ Alle Features (Linien, Symbole, Texte in den Zeichnungen)
- ✅ Alle Journal-Einträge
- ✅ Alle Benutzer, Tenants, etc.
### Was ist mit den Zeichnungen?
Die Zeichnungen sind intakt, aber die Symbole darin zeigen 404. Jedes Symbol in einer Zeichnung speichert in den Properties:
```json
{
"iconId": "alte-gelöschte-uuid",
"imageUrl": ""
}
```
Da die `iconId` auf einen gelöschten Eintrag verweist, kann das Bild nicht mehr geladen werden.
---
## Fix (bereits gepusht)
**Commit:** `5adadd2`
**Änderungen:**
1. `deleteMany` aus `seed.js` und `seed-icons-only.js` entfernt
2. Stattdessen **Upsert-Logik**: Update by `fileKey`, create only if missing
3. Dadurch bleiben bestehende IDs erhalten, und es gibt keine Cascade-Löschungen mehr
4. `prisma/migrate.js` um neue Phase-1-Tabellen erweitert
**Das verhindert zukünftige Löschungen, stellt aber bereits gelöschte Daten NICHT wieder her.**
---
## Wiederherstellung
Um die Symbole und Zeichnungen wiederherzustellen, gibt es zwei Wege:
### Option A: Datenbank-Backup (empfohlen)
Falls ein `pg_dump` oder Snapshot vor dem 20. Mai existiert, können die Tabellen `icon_assets`, `icon_categories` und `tenant_symbols` daraus restored werden.
### Option B: Recovery-Skript
Ein Skript, das:
1. Alle System-Icons aus `public/signaturen/` neu in die DB einspielt
2. Für jeden Tenant die Standard-Symbole neu aktiviert
3. Die Zeichnungen scannt und die `iconId`-Referenzen auf die neuen IDs updated
---
## Lessons Learned
1. **Seed-Skripte dürfen niemals `deleteMany` auf verknüpfte Daten ausführen**
2. `onDelete: Cascade` ist gefährlich bei Daten, die von Benutzern referenziert werden
3. Container-Start-Skripte müssen idempotent sein (mehrfaches Ausführen = gleiches Ergebnis)
4. Vor Deployment-Änderungen sollte ein DB-Backup gemacht werden
---
## Nächste Schritte
1. [x] Recovery-Skript erstellt: `prisma/recover-symbols.js` (Sidebar/Admin)
2. [x] Recovery-Skript erstellt: `prisma/recover-features.js` (Zeichnungen)
3. [x] Renderer resilient gemacht: broken Symbole zeigen ⚠️ statt leeres Nichts
4. [x] `onDelete: Cascade``onDelete: SetNull` auf TenantSymbol.icon geändert
5. [x] Seed-Skripte auf Upsert umgestellt (Commit 5adadd2)
6. [ ] `recover-symbols.js` auf Server ausführen
7. [ ] `recover-features.js --dry-run` auf Server ausführen zur Analyse
8. [ ] Falls broken Features: User informieren (Symbole manuell neu platzieren)

121
ROADMAP.md Normal file
View File

@@ -0,0 +1,121 @@
# Lageplan Verbesserungs-Fahrplan
Priorisierter Plan zur Weiterentwicklung. Wir arbeiten ihn Punkt für Punkt ab.
Legende Aufwand: 🟢 klein · 🟡 mittel · 🔴 gross
---
## Phase 0 Fundament & Sicherheit
- [x] **0.1 JWT-Secret härten** 🟢 ✅
- Gemeinsames Secret-Modul; in Produktion hart fehlschlagen, wenn `NEXTAUTH_SECRET`
fehlt oder < 32 Zeichen. Kein öffentlich bekannter Fallback mehr.
- Betrifft: `src/lib/auth.ts`, `src/middleware.ts`
- _Hinweis: Cookie-Flags (httpOnly/secure/sameSite) sind bereits korrekt._
- [x] **0.2 Token-Laufzeit verkürzt** 🟢 ✅
- „Angemeldet bleiben" 30 → 14 Tage. Laufzeiten zentralisiert (`SESSION_MAX_AGE_*` in `auth.ts`).
- _Offen für später: automatische Verlängerung bei Aktivität + serverseitiger Widerruf (grösserer Umbau, braucht Live-Test)._
- [x] **0.3 Benutzer-/Mandanten-Verwaltung** 🟡 ✅
- Super-Admin sieht ALLE Benutzer über alle Mandanten (Mandant-Spalte, letzte
Anmeldung, Status), sperren/entsperren (`User.isActive`), Passwort-Reset, **Suche**.
- **Anlegen mit Mandanten-Auswahl** (verhindert verwaiste User).
- `admin/page.tsx` von 1365 → ~230 Zeilen entwirrt: je Bereich eine eigene
Komponente (tenants/users/projects/icons/categories-tab), toter Code entfernt.
- Offen (später): Rollen-Dualität (User.role vs. Membership.role) vereinheitlichen.
## Phase 1 Einsatz vs. Übung
- [x] **1.1 `mode`-Feld am Projekt** 🟢 ✅
- Feld `mode` (EINSATZ/UEBUNG) im Schema + Auto-Migration + Validierung.
- Auswahl beim Erstellen (Segmented-Control), Badge in Projektliste, eigene Nummer `Ü-…`.
- [x] **1.2 UI je nach Modus** 🟡 ✅
- Direkte Buttons „Neuer Einsatz" (rot) / „Neue Übung" (blau) in der Topbar.
- Übung → Übungs-Cockpit statt Journal: Übungsziele + Zielerreichung + Auswertung.
- Deutliches „ÜBUNG"-Banner, Sidebar-Tab heisst bei Übung „Auswertung".
## Phase 2 Symbole aufräumen & ergänzen
- [ ] **2.1 Kategorien vereinheitlichen** 🟢 (Motorspritze, Hydrant, Leitern …)
- [ ] **2.2 Fehlende taktische Zeichen** 🟡 (Warteraum, Rettungsachse … Input von Fabian)
- [ ] **2.3 Symbol-Schönheitsfehler** 🟢 (Eingang/Treppe Umriss, Absperrung Seitenlinien)
- [ ] **2.4 Custom-Felder pro Symbol** 🟢 (z. B. Stockwerke; `Item.properties` existiert)
## Phase 3 Lage-/Rapportansicht
- [ ] **3.1 Reduzierte Lage-View** 🟡
- Read-Only (Pendenzen + Stand + Aufgaben) für Grossbildschirm. `Rapport` + Token existieren.
## Phase 4 Echte Kollaboration
- [x] **4.1 Karte & Journal parallel bearbeitbar** 🟡 ✅
- Editor-Lock gilt nur noch für die Karte; Journal/Cockpit unabhängig editierbar.
- Live-Sync für Journal existierte bereits (Socket → journal-refresh).
- _Offen (Kür): echtes gleichzeitiges Zeichnen (Co-Drawing) — bewusst nicht gebaut._
## Zusatz Robustheit / Datensicherheit ✅
- [x] Features-Speichern atomar (`prisma.$transaction`) — kein Totalverlust-Fenster mehr
- [x] localStorage-Features pro Projekt (`lageplan-features-<id>`) statt globalem Key
- [x] Sync-Queue verwirft dauerhafte 4xx-Fehler (kein Endlos-Retry/verstopfte Queue)
- [x] `beforeunload` persistiert auch leere Listen (gelöschte Elemente gehen nicht verloren)
- [x] **5: Optimistische Nebenläufigkeit**`Project.featuresVersion` + Versions-Check
beim Speichern. Ein veralteter (z.B. offline) Stand überschreibt neuere Änderungen nicht
mehr still, sondern erzeugt 409 → Client übernimmt Server-Stand + Warnung. Offline-Queue
verwirft konfliktierende 4xx. (Merge pro Element / echtes Co-Drawing weiterhin offen.)
## Zusatz Leitungsrechner professionalisiert ✅
- [x] Formel zentralisiert in `src/lib/hose-calc.ts` (eine Quelle, typisiert, testbar)
- [x] Ergebnis-Panel als React-Komponente (`MeasurePanel`) statt innerHTML/Inline-CSS
- [x] Schlauchtyp wählbar (Standard ★ vorausgewählt) statt alle gestapelt
- [x] Strahlrohrdruck einstellbar (4/5/6/8 bar)
- [x] Schlauchanzahl aus `lengthPerPieceM` + 10% Verlegereserve (Feld war vorher tot)
- [x] Ehrliche Höhendaten-Anzeige (Quelle bzw. Warnung „keine Höhendaten")
- Offen (später): Berechnungsparameter pro Mandant, mehrere Leitungen/Pumpenkette,
Elevation-Proxy mit Cache, Ergebnis als speicherbares Lageplan-Objekt.
## Zusatz Modul-Baukasten (Einsatz-Cockpit) ✅ v1.5.0
- [x] Journal-Monolith zerlegt: SOMA + Pendenzen als eigenständige Module,
gemeinsame Typen, Karten-Hülle (`ModuleCard`)
- [x] Modul-Registry (`src/lib/modules.ts`) + Konfiguration pro Mandant
(`tenants.modulesConfig`), API `GET/PUT /api/tenant/modules`
- [x] „Module verwalten"-Dialog: an/aus, Reihenfolge, eigene Module erstellen
- [x] Generisches Tabellen-Modul (Spaltentypen Text/Haken/Zeit) mit eigener
Daten-API (`module_items`), Live-Sync über journal-refresh
- [x] Vorlagen: Checkliste, Atemschutz-Überwachung, Kräfte vor Ort,
Lagemeldungen, leeres Modul
- Offen (später): Übungs-Cockpit auf denselben Baukasten umstellen,
Modul-Daten in Rapport-PDF übernehmen
## Zusatz Offline-Karten ✅ v1.5.1
- [x] Kacheln des Einsatzgebiets werden beim Öffnen eines Einsatzes automatisch
vorgeladen (Zoom 1318, ~800 m Radius, max. 400 Kacheln, Parallelität 6)
- [x] Nutzt exakt das MapLibre-Subdomain-Schema `(x+y) % 3` → Cache-Keys passen
- [x] Bereits gecachte Kacheln werden übersprungen; Marker pro Einsatz (14 Tage)
- [x] SW cachte Kacheln schon Cache-First — jetzt auch nie besuchte Bereiche da
- Offen (später): Satellit/Swisstopo vorladen, grösserer Radius wählbar
## Zusatz Krokier-Lagebild ✅ v1.6.0
- [x] **Nordpfeil** als Karten-Overlay, dreht mit dem Bearing mit
- [x] **Windrichtung** (8 Richtungen) setzbar, am Projekt gespeichert
(`Project.windDirection`), als Windrose mit Drift-Pfeil angezeigt
- [x] **Export als Dokument**: Nordpfeil + Massstabsbalken + Windanzeige werden
in PNG & PDF eingebrannt; PDF zusätzlich mit **Symbol-Legende** (Namen aus
/api/icons) und bestehender Kopfzeile (Einsatz/Ort/Zeit/Nr)
- Offen (später): Absperr-/Gefahrenkreis mit Meter-Eingabe, Einheiten
beschriften + Favoriten-Leiste, normgerechte taktische Zeichen
## Phase 5 Kür
- [x] **5.1 Linien-Typ-Abfrage** 🟡 ✅
- Beim Zeichnen einer Linie/eines Pfeils fragt der Dialog nun den Typ ab:
**Normal** (gewählte Farbe), **Leitung** (blau, dicker), **Rettungsachse** (grün,
gestrichelt als freizuhaltende Achse klar erkennbar).
- Typ in `properties.lineType`; eigener MapLibre-Layer mit Dash-Muster für Rettungsachse.
---
_Fortschritt wird hier laufend abgehakt._

123
build-log.txt Normal file
View File

@@ -0,0 +1,123 @@
▲ Next.js 15.5.12
- Environments: .env
- Experiments (use with caution):
· serverActions
Creating an optimized production build ...
✓ Compiled successfully in 5.8s
Skipping validation of types
Linting ...
Collecting page data ...
⚠ Using edge runtime on a page currently disables static generation for that page
Generating static pages (0/53) ...
Generating static pages (13/53)
Generating static pages (26/53)
Generating static pages (39/53)
✓ Generating static pages (53/53)
Finalizing page optimization ...
Collecting build traces ...
Route (app) Size First Load JS
┌ ○ / 3.03 kB 361 kB
├ ○ /_not-found 1.02 kB 340 kB
├ ƒ /[slug] 3.52 kB 348 kB
├ ○ /admin 35.3 kB 424 kB
├ ƒ /api/admin/categories 325 B 340 kB
├ ƒ /api/admin/categories/[id] 325 B 340 kB
├ ƒ /api/admin/icons 325 B 340 kB
├ ƒ /api/admin/icons/[id] 325 B 340 kB
├ ƒ /api/admin/icons/upload 325 B 340 kB
├ ƒ /api/admin/projects 325 B 340 kB
├ ƒ /api/admin/settings 325 B 340 kB
├ ƒ /api/admin/tenants 325 B 340 kB
├ ƒ /api/admin/tenants/[id] 325 B 340 kB
├ ƒ /api/admin/tenants/[id]/logo 325 B 340 kB
├ ƒ /api/admin/tenants/[id]/logo/serve 325 B 340 kB
├ ƒ /api/admin/tenants/[id]/members 325 B 340 kB
├ ƒ /api/admin/trial-reminders 325 B 340 kB
├ ƒ /api/admin/users 325 B 340 kB
├ ƒ /api/admin/users/[id] 325 B 340 kB
├ ƒ /api/admin/users/[id]/reset-password 325 B 340 kB
├ ƒ /api/auth/change-password 325 B 340 kB
├ ƒ /api/auth/delete-account 325 B 340 kB
├ ƒ /api/auth/forgot-password 325 B 340 kB
├ ƒ /api/auth/login 325 B 340 kB
├ ƒ /api/auth/logout 325 B 340 kB
├ ƒ /api/auth/me 325 B 340 kB
├ ƒ /api/auth/register 325 B 340 kB
├ ƒ /api/auth/resend-verification 325 B 340 kB
├ ƒ /api/auth/reset-password 325 B 340 kB
├ ƒ /api/auth/verify-email 325 B 340 kB
├ ƒ /api/contact 325 B 340 kB
├ ƒ /api/demo 325 B 340 kB
├ ƒ /api/dictionary 325 B 340 kB
├ ƒ /api/dictionary/[id] 325 B 340 kB
├ ƒ /api/donate/checkout 325 B 340 kB
├ ƒ /api/donate/config 325 B 340 kB
├ ƒ /api/donate/webhook 325 B 340 kB
├ ƒ /api/hose-types 325 B 340 kB
├ ƒ /api/hose-types/[id] 325 B 340 kB
├ ƒ /api/icons 325 B 340 kB
├ ƒ /api/icons/[id]/image 325 B 340 kB
├ ƒ /api/icons/[id]/toggle-visibility 325 B 340 kB
├ ƒ /api/icons/upload 325 B 340 kB
├ ƒ /api/projects 325 B 340 kB
├ ƒ /api/projects/[id] 325 B 340 kB
├ ƒ /api/projects/[id]/editing 325 B 340 kB
├ ƒ /api/projects/[id]/export 325 B 340 kB
├ ƒ /api/projects/[id]/features 325 B 340 kB
├ ƒ /api/projects/[id]/journal 325 B 340 kB
├ ƒ /api/projects/[id]/journal/check-items 325 B 340 kB
├ ƒ /api/projects/[id]/journal/check-items/[itemId] 325 B 340 kB
├ ƒ /api/projects/[id]/journal/entries 325 B 340 kB
├ ƒ /api/projects/[id]/journal/entries/[entryId] 325 B 340 kB
├ ƒ /api/projects/[id]/journal/pendenzen 325 B 340 kB
├ ƒ /api/projects/[id]/journal/pendenzen/[pendenzId] 325 B 340 kB
├ ƒ /api/projects/[id]/journal/send-report 325 B 340 kB
├ ƒ /api/projects/[id]/plan-image 325 B 340 kB
├ ƒ /api/projects/[id]/plan-image/serve 325 B 340 kB
├ ƒ /api/rapports 325 B 340 kB
├ ƒ /api/rapports/[token] 325 B 340 kB
├ ƒ /api/rapports/[token]/pdf 325 B 340 kB
├ ƒ /api/rapports/[token]/send 325 B 340 kB
├ ƒ /api/settings/public 325 B 340 kB
├ ƒ /api/templates 325 B 340 kB
├ ƒ /api/templates/import 325 B 340 kB
├ ƒ /api/tenant/categories 325 B 340 kB
├ ƒ /api/tenant/delete 325 B 340 kB
├ ƒ /api/tenant/info 325 B 340 kB
├ ƒ /api/tenant/logo 325 B 340 kB
├ ƒ /api/tenant/soma-templates 325 B 340 kB
├ ƒ /api/tenant/symbols 325 B 340 kB
├ ƒ /api/tenant/symbols/[id]/image 325 B 340 kB
├ ƒ /api/tenants/[tenantId]/suggestions 325 B 340 kB
├ ƒ /api/tenants/by-slug/[slug] 325 B 340 kB
├ ƒ /api/upgrade-requests 325 B 340 kB
├ ƒ /api/upgrade-requests/[id] 325 B 340 kB
├ ○ /app 281 kB 879 kB
├ ○ /datenschutz 3.18 kB 347 kB
├ ○ /demo 8.78 kB 555 kB
├ ○ /forgot-password 3.88 kB 361 kB
├ ○ /impressum 3.01 kB 353 kB
├ ○ /login 5.74 kB 363 kB
├ ƒ /opengraph-image 325 B 340 kB
├ ƒ /rapport/[token] 4.83 kB 344 kB
├ ○ /register 5.48 kB 363 kB
├ ○ /reset-password 4.01 kB 362 kB
├ ○ /robots.txt 325 B 340 kB
├ ○ /settings 4.28 kB 356 kB
├ ○ /sitemap.xml 325 B 340 kB
├ ○ /spenden 5.26 kB 363 kB
└ ○ /spenden/danke 2.54 kB 360 kB
+ First Load JS shared by all 339 kB
├ chunks/1255-94429a3f41c08b44.js 65.5 kB
├ chunks/4bd1b696-100b9d70ed4e49c1.js 54.2 kB
├ chunks/ed9f2dc4-1b30afa125168b53.js 217 kB
└ other shared chunks (total) 2.21 kB
ƒ Middleware 40.1 kB
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand

File diff suppressed because one or more lines are too long

View File

@@ -1,102 +1,168 @@
# Lageplan — Portainer Deployment
# Lageplan — CI/CD & Portainer Deployment
## Architektur
## Übersicht
```
Browser → :3000 (Web App) → intern: db:5432, minio:9000
┌─────────────┐ Push ┌─────────────────┐ Build + Push ┌─────────────────┐
│ Dein PC │ ────────────► │ Gitea (git) │ ──────────────────► │ Gitea Registry │
│ (VS Code) │ │ git.purepixel │ │ (Docker Image) │
└─────────────┘ └─────────────────┘ └────────┬────────┘
│ Pull
┌─────────────────┐
│ Portainer │
│ (Stack Deploy) │
└─────────────────┘
```
Nur **ein Port (3000)** muss exponiert werden. DB und MinIO laufen rein intern im Docker-Netzwerk. Icons werden über die Web-App gestreamt — kein direkter MinIO-Zugriff nötig.
1. **Push auf `main`** → Gitea Actions baut Docker Image
2. **Image wird gepusht** → Gitea Container Registry (`git.purepixel.ch`)
3. **Watchtower (in Portainer)** → prüft alle 60s auf neue Images und startet Container neu
---
## Dateien
## Voraussetzungen
| Datei | Beschreibung |
|-------|-------------|
| `lageplan-web-v1.0.0.tar` | Docker Image (~92 MB) |
| `portainer-stack.yml` | Stack YAML für Portainer |
| `.env.example` | Environment Variables Vorlage |
- Gitea läuft mit Container Registry aktiviert
- Gitea Actions Runner ist registriert (`deploy/docker-compose.runner.yml`)
- Portainer Stack ist deployed mit korrekten Environment-Variablen
---
## Schritt 1: Image auf Server laden
## Schritt 1: Gitea Container Registry aktivieren
```bash
# Kopieren
scp lageplan-web-v1.0.0.tar user@server:/tmp/
In Gitea:
1. **Admin-Konsole****Konfiguration****Pakete**
2. **Container Registry** auf `Aktiviert` setzen
3. Speichern
# Auf dem Server laden
docker load -i /tmp/lageplan-web-v1.0.0.tar
Oder direkt in der `app.ini`:
```ini
[packages]
ENABLED = true
# Prüfen
docker images | grep lageplan
[package.container_registry]
ENABLED = true
```
---
## Schritt 2: Stack in Portainer erstellen
## Schritt 2: Gitea Actions Runner registrieren
1. In Gitea: **Admin****Actions****Runners****Neuen Runner erstellen**
2. Token kopieren
3. In Portainer: Stack `gitea-runner` deployen mit [`deploy/docker-compose.runner.yml`](docker-compose.runner.yml)
4. Environment Variable `RUNNER_TOKEN` = das kopierte Token
---
## Schritt 3: Gitea Access Token erstellen
Das CI/CD Workflow braucht einen Token um Images in die Registry zu pushen:
1. Gitea → **Einstellungen****Anwendungen****Token erstellen**
2. Name: `registry-push`
3. Berechtigungen: `package:write` (mindestens)
4. Token kopieren und als **Repository Secret** hinterlegen:
- Repo → **Einstellungen****Secrets****Neues Secret**
- Name: `GITEA_TOKEN`
- Wert: das kopierte Token
---
## Schritt 4: Portainer Stack deployen
1. **Portainer**`Stacks``+ Add stack`
2. **Name**: `lageplan`
3. **Web editor**: Inhalt von `portainer-stack.yml` einfügen
4. **Environment variables** setzen:
3. **Build method**: `Repository`
4. **Git-URL**: `https://git.purepixel.ch/adminpepe/Lageplan.git`
5. **Compose path**: `docker-compose.portainer.yml`
6. **GitOps updates**: ✅ Aktivieren
7. **Mechanism**: `Webhook`
8. **Webhook URL kopieren** (für später)
| Variable | Wert |
|----------|------|
| `POSTGRES_USER` | `lageplan` |
| `POSTGRES_PASSWORD` | *(sicheres Passwort)* |
| `POSTGRES_DB` | `lageplan` |
| `MINIO_ROOT_USER` | `minioadmin` |
| `MINIO_ROOT_PASSWORD` | *(sicheres Passwort)* |
| `MINIO_BUCKET` | `lageplan-icons` |
| `WEB_PORT` | `3000` |
| `NEXTAUTH_URL` | `http://SERVER_IP:3000` |
| `NEXTAUTH_SECRET` | *(langer zufälliger String)* |
### Environment Variables setzen:
> **Tipp**: Secret generieren: `openssl rand -base64 32`
| Variable | Wert | Beschreibung |
|----------|------|-------------|
| `POSTGRES_USER` | `lageplan` | DB User |
| `POSTGRES_PASSWORD` | *(sicheres Passwort)* | DB Passwort |
| `POSTGRES_DB` | `lageplan` | DB Name |
| `NEXTAUTH_URL` | `https://lageplan.ch` | Deine Domain |
| `NEXTAUTH_SECRET` | *(openssl rand -base64 32)* | Auth Secret |
| `MINIO_ROOT_USER` | `minioadmin` | MinIO User |
| `MINIO_ROOT_PASSWORD` | *(sicheres Passwort)* | MinIO Passwort |
| `MINIO_BUCKET` | `lageplan-icons` | Bucket Name |
| `MINIO_PUBLIC_URL` | `https://s3.deinedomain.ch` | MinIO externe URL |
| `GITEA_REGISTRY_USER` | `adminpepe` | Gitea User für Watchtower |
| `GITEA_REGISTRY_PASS` | *(Token oder Passwort)* | Gitea Passwort/Token |
5. **Deploy the stack**
9. **Deploy the stack**
---
## Schritt 3: Datenbank initialisieren (einmalig)
## Schritt 5: Webhook in Gitea eintragen
In Portainer: Container `web` → Console → `/bin/sh`:
Damit Portainer bei jedem Push automatisch neu deployed:
```bash
npx prisma db push
npx prisma db seed
```
Oder per SSH:
```bash
docker exec -it lageplan-web-1 npx prisma db push
docker exec -it lageplan-web-1 npx prisma db seed
```
1. Gitea Repo → **Einstellungen****Webhooks****Neuer Webhook**`Gitea`
2. **Ziel-URL**: Die kopierte Portainer Webhook URL
3. **HTTP-Methode**: `POST`
4. **Trigger**: Nur `Push events` (oder auch `Branch filter: main`)
5. **Webhook aktivieren** → Hinzufügen
---
## Schritt 4: Zugriff
## Schritt 6: Erstes Deployment testen
- **Web App**: `http://SERVER_IP:3000`
- **Login**: `admin@lageplan.local` / `admin123`
1. Lokal einen Push auf `main` machen:
```bash
git add .
git commit -m "Test CI/CD"
git push origin main
```
2. In Gitea: **Actions** Tab → Build-Job sollte laufen
3. Wenn grün → Image wurde in Registry gepusht
4. Watchtower (in Portainer) holt neues Image innerhalb von 60s
5. App ist unter `NEXTAUTH_URL` erreichbar
---
## Update
## Troubleshooting
### Gitea Actions startet nicht
- Prüfen ob Runner registriert ist: Gitea → Admin → Actions → Runners
- Runner muss `Idle` oder `Active` zeigen
### Image Push schlägt fehl (401 Unauthorized)
- `GITEA_TOKEN` Secret im Repo korrekt hinterlegt?
- Token hat Berechtigung `package:write`?
- Registry in Gitea aktiviert?
### Watchtower zieht kein neues Image
- `GITEA_REGISTRY_USER` und `GITEA_REGISTRY_PASS` in Portainer gesetzt?
- Image-Name in `docker-compose.portainer.yml` korrekt?
- Watchtower Logs prüfen: Portainer → Container `watchtower` → Logs
### App startet nicht / DB-Fehler
- Environment Variables in Portainer korrekt?
- `DATABASE_URL` wird automatisch gebaut, nur `POSTGRES_*` muss gesetzt werden
- Bei erstem Start: Prisma Migrations/Seed im Web-Container ausführen:
```bash
docker exec -it lageplan-web-1 npx prisma db push
docker exec -it lageplan-web-1 npx prisma db seed
```
---
## Manuelles Update (falls nötig)
Wenn Watchtower mal nicht greift:
```bash
# Lokal: neues Image bauen + exportieren
docker compose build web
docker tag lageplan-web:latest lageplan-web:v1.1.0
docker save lageplan-web:v1.1.0 -o deploy/lageplan-web-v1.1.0.tar
# Server: laden
docker load -i lageplan-web-v1.1.0.tar
# Portainer: Stack → Editor → Image-Tag ändern → Update the stack
# Auf dem Portainer-Host
docker pull git.purepixel.ch/adminpepe/lageplan:latest
docker compose -f docker-compose.portainer.yml up -d web
```
---

View File

@@ -0,0 +1,23 @@
##############################################
# Gitea Actions Runner — Portainer Stack
#
# In Portainer deployen:
# 1. Stacks → Add Stack → "gitea-runner"
# 2. Diese YAML einfügen
# 3. Deploy
##############################################
services:
runner:
image: gitea/act_runner:latest
restart: unless-stopped
environment:
GITEA_INSTANCE_URL: https://git.purepixel.ch
GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN}
GITEA_RUNNER_NAME: lageplan-runner
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- runner_data:/data
volumes:
runner_data:

View File

@@ -7,3 +7,8 @@ MINIO_BUCKET=lageplan-icons
WEB_PORT=3000
NEXTAUTH_URL=http://SERVER_IP:3000
NEXTAUTH_SECRET=HIER_LANGEN_ZUFAELLIGEN_STRING_GENERIEREN
MINIO_PUBLIC_URL=http://SERVER_IP:9000
# Gitea Registry Auth für Watchtower (automatische Image-Updates)
GITEA_REGISTRY_USER=adminpepe
GITEA_REGISTRY_PASS=HIER_GITEA_TOKEN_ODER_PASSWORT_SETZEN

View File

@@ -1,5 +1,5 @@
##############################################
# Gitea — Lightweight Git Server
# Gitea — Lightweight Git Server + Container Registry
#
# Verwendung in Portainer:
# 1. Stacks → Add Stack → "Gitea"
@@ -12,6 +12,10 @@
# 3. Repository "lageplan" erstellen
# 4. Vom PC aus: git init → git remote add origin → git push
#
# Container Registry aktivieren:
# 1. Gitea Admin → Konfiguration → Pakete → Container Registry aktivieren
# 2. Oder app.ini: [packages] ENABLED = true
#
# Daten werden in gitea_data persistiert.
##############################################
@@ -27,6 +31,9 @@ services:
- GITEA__server__ROOT_URL=https://git.purepixel.ch
- GITEA__server__HTTP_PORT=3000
- GITEA__server__LFS_START_SERVER=true
# Container Registry aktivieren
- GITEA__packages__ENABLED=true
- GITEA__package__container_registry__ENABLED=true
volumes:
- gitea_data:/data
- /etc/timezone:/etc/timezone:ro
@@ -43,4 +50,3 @@ volumes:
networks:
lageplan_lageplan-net:
external: true

View File

@@ -1,21 +1,28 @@
##############################################
# Lageplan — Portainer Stack Configuration
#
# Verwendung in Portainer:
# 1. Stacks → Add Stack
# 2. "Upload" oder diesen Inhalt einfügen
# 3. Environment-Variablen setzen (siehe unten)
# 4. Deploy
# Lageplan — Portainer Stack (Watchtower Auto-Update)
#
# Setup in Portainer:
# 1. Stacks → Add Stack → "Repository"
# 2. Git-URL: https://git.purepixel.ch/adminpepe/Lageplan.git
# 3. Compose-Pfad: docker-compose.portainer.yml
# 4. "GitOps updates" aktivieren
# 5. Environment-Variablen setzen (siehe unten)
# 6. Deploy
#
# Danach: Push auf main → Gitea Actions baut Image →
# Watchtower erkennt neues Image und startet Container neu
#
# Benötigte Environment-Variablen:
# POSTGRES_USER (default: lageplan)
# POSTGRES_PASSWORD (ÄNDERN!)
# POSTGRES_DB (default: lageplan)
# NEXTAUTH_SECRET (ÄNDERN! — z.B. openssl rand -base64 32)
# NEXTAUTH_URL (z.B. https://lageplan.example.com)
# NEXTAUTH_URL (z.B. https://lageplan.ch)
# MINIO_ROOT_USER (default: minioadmin)
# MINIO_ROOT_PASSWORD (ÄNDERN!)
# MINIO_PUBLIC_URL (z.B. https://s3.example.com)
# GITEA_REGISTRY_USER (für Watchtower Registry-Auth)
# GITEA_REGISTRY_PASS (für Watchtower Registry-Auth)
##############################################
services:
@@ -76,13 +83,14 @@ services:
- lageplan
# ─── Lageplan Web App ──────────────────────
# Image kommt aus Gitea Container Registry (gebaut via Gitea Actions)
web:
image: 192.168.1.183:3100/adminpepe/lageplan:latest
image: git.purepixel.ch/adminpepe/lageplan:latest
restart: unless-stopped
environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-lageplan}:${POSTGRES_PASSWORD:-lageplan_secret}@db:5432/${POSTGRES_DB:-lageplan}
NEXTAUTH_URL: ${NEXTAUTH_URL:-https://localhost:3000}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-super-secret-key-change-in-production}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:?NEXTAUTH_SECRET muss gesetzt sein (mind. 32 Zeichen) - z.B. via stack.env}
MINIO_ENDPOINT: minio
MINIO_PORT: "9000"
MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-minioadmin}
@@ -99,6 +107,25 @@ services:
condition: service_healthy
networks:
- lageplan
labels:
- "com.centurylinklabs.watchtower.enable=true"
# ─── Watchtower (Auto-Restart bei neuen Images) ─
# Überwacht nur Container mit Label com.centurylinklabs.watchtower.enable=true
watchtower:
image: containrrr/watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
WATCHTOWER_POLL_INTERVAL: 60
WATCHTOWER_CLEANUP: "true"
WATCHTOWER_LABEL_ENABLE: "true"
# Gitea Registry Auth
REPO_USER: ${GITEA_REGISTRY_USER}
REPO_PASS: ${GITEA_REGISTRY_PASS}
networks:
- lageplan
volumes:
postgres_data:

View File

@@ -66,7 +66,7 @@ services:
environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-lageplan}:${POSTGRES_PASSWORD:-lageplan_secret}@db:5432/${POSTGRES_DB:-lageplan}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-super-secret-key-change-in-production}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:?NEXTAUTH_SECRET muss gesetzt sein (mind. 32 Zeichen) - z.B. via .env oder stack.env}
MINIO_ENDPOINT: minio
MINIO_PORT: 9000
MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-minioadmin}

View File

@@ -5,7 +5,11 @@ echo "=== Lageplan Startup ==="
# ─── Step 1: Run migrations (uses PrismaClient raw SQL, no CLI needed) ───
echo "[1/3] Running database migrations..."
node prisma/migrate.js || echo " Warning: migrations had issues (may be first run)"
node prisma/migrate.js
if [ $? -ne 0 ]; then
echo "❌ Database migrations failed. Aborting startup."
exit 1
fi
# ─── Step 2: Conditional seeding ───
echo "[2/3] Checking if seeding is needed..."
@@ -34,6 +38,44 @@ else
fi
fi
# ─── Step 3: Start server ───
# ─── Step 3: Seed SymbolTemplates (idempotent) ───
echo "[2b/3] Seeding symbol templates..."
node -e "
const { PrismaClient } = require('@prisma/client');
const fs = require('fs');
const path = require('path');
const prisma = new PrismaClient();
async function seedTemplates() {
const sigDir = path.join(process.cwd(), 'public', 'signaturen');
let files = [];
try { files = fs.readdirSync(sigDir).filter(f => f.endsWith('.svg')).sort(); } catch(e) { console.log('No signaturen dir'); }
let created = 0, skipped = 0;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const displayName = file.replace(/\.svg$/i, '').replace(/[_-]/g, ' ').trim();
const fileKey = 'signaturen/' + file;
const existing = await prisma.symbolTemplate.findFirst({ where: { svgPath: fileKey } }).catch(() => null);
if (existing) { skipped++; continue; }
await prisma.symbolTemplate.create({
data: {
packageId: 'feuerwehr-ch',
packageName: 'Feuerwehr Schweiz',
categoryName: 'Sonstiges',
name: displayName,
svgPath: fileKey,
tags: [displayName.toLowerCase()],
sortOrder: i,
}
}).catch(() => {});
created++;
}
console.log('Templates: ' + created + ' created, ' + skipped + ' skipped');
await prisma.\$disconnect();
}
seedTemplates().catch(() => { console.log('Template seed skipped'); prisma.\$disconnect(); });
" || echo " Warning: template seed failed"
# ─── Step 4: Start server ───
echo "[3/3] Starting server..."
exec node server-custom.js

49
docs/TESTPLAN.md Normal file
View File

@@ -0,0 +1,49 @@
# Handy-/Einsatz-Testplan
Kurzer, strukturierter Durchlauf nach jedem grösseren Deploy — am besten einmal
am **Handy** und einmal am **Desktop**. Abhaken, was funktioniert.
## 1. Einsatz anlegen
- [ ] 🔥 **Neu → Neuer Einsatz**: Dialog öffnet, Standort wird automatisch als Ort vorgeschlagen
- [ ] Titel eingeben → **Erstellen** → Karte startet am Einsatzort
- [ ] Kopfzeile zeigt den Einsatz mittig (Status-Punkt rot, Nr, Titel, Ort), Uhr rechts
## 2. Karte & Symbole (der wichtige Handy-Test)
- [ ] Symbol **antippen** → Hinweis erscheint → **auf Karte tippen** platziert es
- [ ] Mehrere gleiche Symbole nacheinander setzen → Zähler steigt → **Fertig**
- [ ] Symbol antippen (auf der Karte) → drehen/skalieren/löschen
- [ ] Zeichnen: Pfeil/Linie ziehen → Dialog fragt **Normal / Leitung / Rettungsachse**
- [ ] Eigene **GPS-Position** sichtbar (Punkt + Genauigkeit), Karte bleibt am Einsatzort
- [ ] **Vollbild-Button** (oben links) blendet die Browser-Leiste aus
## 3. Auto-Bearbeitung
- [ ] Ohne „Bearbeitung starten" direkt zeichnen können (Lock wird automatisch übernommen)
- [ ] Zweites Gerät/Person: sieht Änderungen live, Karte ist dort gesperrt
- [ ] Statuszeile zeigt „Sie bearbeiten" + **freigeben**
## 4. Cockpit-Module (Baukasten)
- [ ] Journal-Tab → **Module** (als Admin): Modul an/aus, Reihenfolge, eigenes Modul erstellen
- [ ] Neues Modul (z.B. Atemschutz) erscheint im Cockpit, Zeilen anlegen/haken/löschen
- [ ] Änderungen live auf zweitem Gerät sichtbar
## 5. Journal & Rapport
- [ ] Journal-Eintrag erfassen (mit Vorschlags-Leiste), Erledigt-Haken, Korrektur
- [ ] SOMA/Pendenzen bedienen
- [ ] **Rapport** erzeugen → PDF enthält Journal, SOMA, Pendenzen **und die eigenen Module**
- [ ] Rapport per E-Mail / Drucken
## 6. Übung
- [ ] 🎓 **Neu → Neue Übung** → Übungs-Cockpit mit Zielen + Auswertung
- [ ] Übungsziele mit Status (Erreicht/Teilweise/…), Auswertung speichert automatisch
- [ ] Eigene Module erscheinen auch in der Übung
## 7. Offline
- [ ] Einsatz öffnen → Toast „Karte offline verfügbar" abwarten
- [ ] Flugmodus an → Karte im Einsatzgebiet bleibt sichtbar, zeichnen möglich
- [ ] Flugmodus aus → Änderungen werden synchronisiert (Statuszeile „Offline" verschwindet)
## 8. Beenden
- [ ] **Einsatz beenden** (Kopfzeile/Menü) → In-App-Bestätigung → gespeichert & geschlossen
---
_Bei Problemen: betroffenen Schritt notieren + ggf. Screenshot, dann gezielt fixen._

247
docs/backup-und-seeding.md Normal file
View File

@@ -0,0 +1,247 @@
# Backup & Seeding — Betriebsnotiz Lageplan
> Stand: 2026-05-20
> Autor: Automatisch generiert nach Produktionsvorfall
## Inhalt
1. [Wichtige Regeln](#wichtige-regeln)
2. [Backup-Strategie](#backup-strategie)
3. [Seed-Skripte](#seed-skripte)
4. [Post-Deploy Prüfung](#post-deploy-prüfung)
5. [Notfall-Recovery](#notfall-recovery)
6. [Docker-Build Hinweise](#docker-build-hinweise)
---
## Wichtige Regeln
### Nie löschen
- **Keine** `deleteMany()`, `DELETE FROM`, `TRUNCATE` oder Cascade-Resets in Production
- **Nie** direkt in `icon_assets`, `icon_categories`, `tenant_symbols`, `features`, `projects` löschen
- Alle Seed-Skripte müssen **idempotent** sein (`upsert` statt `delete` + `create`)
- Vor jedem Seed/Repair in Production: **Databasus-Backup erstellen**
### Databasus Backup
- Databasus ist als GUI-Backup eingerichtet
- Backups werden gespeichert unter: `/volume1/docker/backups/databasus`
- **Nie** das Docker-Volume unter `/volume1/@docker` als einzige Backup-Strategie verwenden
### Container-Informationen
| Service | Container-Name |
|---------|---------------|
| PostgreSQL DB | `lageplan-db-1` |
| Web App | `lageplan-web-1` |
| DB-Name | `lageplan` |
| DB-User | `lageplan` |
---
## Backup-Strategie
### Automatisch (Databasus)
1. Databasus GUI öffnen
2. Datenbank `lageplan` auswählen
3. "Backup Now" klicken
4. Backup wird unter `/volume1/docker/backups/databasus` abgelegt
### Manuell (pg_dump)
```bash
# In der NAS-Shell (oder im DB-Container)
docker exec lageplan-db-1 pg_dump -U lageplan -d lageplan \
--no-owner --no-privileges \
> /volume1/docker/backups/databasus/lageplan-manual-$(date +%Y%m%d-%H%M%S).sql
```
### Wiederherstellen
```bash
# 1. Backup-Datei prüfen
ls -la /volume1/docker/backups/databasus/
# 2. App stoppen
docker stop lageplan-web-1
# 3. DB zurückspielen (ACHTUNG: überschreibt aktuelle Daten!)
docker exec -i lageplan-db-1 psql -U lageplan -d lageplan \
< /volume1/docker/backups/databasus/lageplan-backup-YYYYmmdd-HHMMSS.sql
# 4. App starten
docker start lageplan-web-1
```
---
## Seed-Skripte
### Übersicht
| Skript | Zweck | Sicherheit | Aufruf |
|--------|-------|-----------|--------|
| `prisma/seed.js` | Vollständiger Seed (Tenant, Users, Icons, HoseTypes, Demo-Projekt) | ✅ Idempotent, nur `upsert` | `npm run db:seed` oder `node prisma/seed.js` |
| `prisma/seed-icons-only.js` | Nur Icons aus `public/signaturen/*.svg` | ✅ Idempotent, nur `upsert` | `npm run db:seed:icons` oder `node prisma/seed-icons-only.js` |
| `prisma/seed.ts` | Legacy-TypeScript-Seed (korrigiert, aber nicht der Primary) | ✅ Korrigiert | Nicht direkt aufrufen |
| `prisma/seed-symbol-templates.ts` | Erstellt `SymbolTemplate`-Einträge | ✅ Nur `findFirst` + `create` (skip if exists) | `npx tsx prisma/seed-symbol-templates.ts` |
| `prisma/migrate-tenant-symbols.ts` | Migriert bestehende `TenantSymbol` zu neuem Schema | ✅ Nur `findFirst`, `create`, `update` | `npx tsx prisma/migrate-tenant-symbols.ts` |
| `prisma/recover-symbols.js` | Recovery: Icons + TenantSymbols wiederherstellen | ✅ Dry-Run/Apply Modus | `npm run recover:symbols:dry-run` / `npm run recover:symbols:apply` |
| `prisma/repair-features.js` | Repariert kaputte `iconId`-Referenzen in Features | ✅ Dry-Run/Apply Modus + Backup | `npm run repair:features:dry-run` / `npm run repair:features:apply` |
| `prisma/migrate.js` | DB-Migrationen (Raw SQL, idempotent) | ✅ Keine Deletes auf User-Daten | Wird automatisch beim Container-Start ausgeführt |
### Empfohlene Seed-Strategie
1. **Neue Umgebung** (erstmalig):
```bash
node prisma/seed.js
```
2. **Nur Icons aktualisieren** (z.B. nach SVG-Update):
```bash
node prisma/seed-icons-only.js
```
3. **Recovery nach Icon-Verlust**:
```bash
# 1. Erst Databasus-Backup erstellen!
# 2. Dann:
node prisma/recover-symbols.js --dry-run # Analyse
node prisma/recover-symbols.js --apply # Anwenden
```
---
## Post-Deploy Prüfung
### 1. SVG-Dateien im Container
```bash
docker exec -it lageplan-web-1 sh -c 'find /app/public/signaturen -type f -name "*.svg" | wc -l'
```
**Erwartung:** ca. 117
### 2. Icons seeden
```bash
docker exec -it lageplan-web-1 node prisma/seed-icons-only.js
```
**Erwartung:**
```
✅ 10 icon categories upserted
✅ FKS Signaturen: X created, Y updated (117 total SVGs)
```
### 3. Datenbank-Prüfung
```bash
docker exec -it lageplan-db-1 psql -U lageplan -d lageplan -c "
SELECT 'icon_categories' AS table_name, COUNT(*) AS count FROM icon_categories
UNION ALL
SELECT 'icon_assets' AS table_name, COUNT(*) AS count FROM icon_assets
UNION ALL
SELECT 'tenant_symbols' AS table_name, COUNT(*) AS count FROM tenant_symbols
UNION ALL
SELECT 'features' AS table_name, COUNT(*) AS count FROM features
UNION ALL
SELECT 'projects' AS table_name, COUNT(*) AS count FROM projects;
"
```
### 4. Logs prüfen
```bash
docker logs --tail=100 lageplan-web-1
```
**Keine** der folgenden Fehler sollen auftreten:
- `404` für `/signaturen/*.svg`
- `Prisma error` wegen `tenant_symbols.categoryId`
- `Seed failed`
---
## Notfall-Recovery
### Szenario: Symbole sind verschwunden (404)
**Ursache:** `IconAsset`-Einträge wurden gelöscht (z.B. durch altes Seed-Skript mit `deleteMany`)
**Lösung:**
```bash
# 1. Backup erstellen (Databasus GUI oder pg_dump)
# 2. Container-Status prüfen
docker exec -it lageplan-web-1 sh -c 'ls -la /app/public/signaturen/ | head -5'
# → Sollte SVG-Dateien anzeigen
# 3. Icons neu seeden (idempotent, sicher)
docker exec -it lageplan-web-1 node prisma/seed-icons-only.js
# 4. Tenant-Symbols wiederherstellen (falls auch gelöscht)
docker exec -it lageplan-web-1 node prisma/recover-symbols.js --dry-run
docker exec -it lageplan-web-1 node prisma/recover-symbols.js --apply
# 5. Prüfen
docker exec -it lageplan-web-1 node -e "
const {PrismaClient} = require('@prisma/client');
const p = new PrismaClient();
p.iconAsset.count({where:{isSystem:true}}).then(c=>console.log('System-Icons:',c));
p.tenantSymbol.count().then(c=>console.log('TenantSymbols:',c));
"
```
### Szenario: Features haben kaputte iconId-Referenzen
**Ursache:** Alte `iconId`-UUIDs in `Feature.properties` zeigen auf gelöschte `IconAsset`s
**Lösung:**
```bash
# 1. Dry-Run: Analyse
docker exec -it lageplan-web-1 node prisma/repair-features.js --dry-run
# 2. Apply: Reparatur (erstellt automatisch JSON-Backup)
docker exec -it lageplan-web-1 node prisma/repair-features.js --apply
```
---
## Docker-Build Hinweise
### public/signaturen muss im Image sein
- `public/signaturen/*.svg` muss im Git-Index sein (`.gitignore` erlaubt es explizit)
- `.dockerignore` darf `public/signaturen/` **nicht** ausschliessen
- `Dockerfile` kopiert `public/` in die Runtime-Stage:
```dockerfile
COPY --from=builder /app/public ./public
```
### Next.js standalone
- `next.config.js` hat `output: 'standalone'`
- Der Build erzeugt `.next/standalone/` (minimaler Server)
- `public/` wird explizit in die Runtime-Stage kopiert
### Multi-Stage Build
1. **deps**: `npm ci` (nur Dependencies)
2. **builder**: `npm run build` (inkl. Prisma Generate)
3. **runner**: Runtime-Image mit `public/`, `.next/standalone/`, Prisma-Client
---
## Änderungshistorie
| Datum | Änderung |
|-------|----------|
| 2026-05-20 | `.gitignore` korrigiert: `Signaturen/` → `/Signaturen/` (Root-only), `public/signaturen/*.svg` erlaubt |
| 2026-05-20 | 117 SVG-Dateien force-added zum Git-Index |
| 2026-05-20 | `.dockerignore` korrigiert: `Signaturen` → `/Signaturen/` |
| 2026-05-20 | `package.json`: `db:seed` zeigt jetzt auf `prisma/seed.js` statt broken `seed.ts` |
| 2026-05-20 | `seed.ts` korrigiert: Role-Enums + stabile IDs für `iconCategory.upsert` |
| 2026-05-20 | Alle Seed-Skripte auf `deleteMany`-Freiheit geprüft |
| 2026-05-20 | Dokumentation `docs/backup-und-seeding.md` erstellt |

View File

@@ -0,0 +1,83 @@
# Compliance-Umsetzung — Abschlussbericht
Stand: 2026-07-23 · Versionen 1.7.0 1.7.4 · sorgfältiger Entwurf, ersetzt keine juristische Prüfung.
## A. Zusammenfassung
- **Analyse**: 4 parallele Analysen (Legal/Claims, Dienstleister, Auth/Modelle/Admin, Export/Security/Cookies).
- **Claims**: 20 werbliche/absolute Aussagen korrigiert (24/7, 100 %, DSG/DSGVO-konform, „keine Weitergabe",
„keine Übermittlung ins Ausland", „verschlüsselt"→„gehasht", 100 % der Spende, „Kein Unternehmen").
- **Transparenz**: zentrale Legal-Config, neue Rechtsseiten, faktenbasierte Datenschutzerklärung mit
echter Subprozessorenliste.
- **Consent**: revisionssichere Zustimmungsprotokollierung (LegalDocument/LegalAcceptance), Registrierung
mit getrennten Zustimmungen, blockierender Re-Consent-Dialog, Organisationsbestätigung (7 Pflicht-Checks),
Admin-Übersicht mit CSV-Export, Konto-Datenschutzseite.
- **Risikoreduktion**: Export-Prüfhinweise (PDF/PNG), Dashboard-Hinweis, Berechnungs-/Geodaten-Hinweis,
private Routen `noindex`/`no-store`.
## B. Geänderte/neue Dateien (Auszug)
| Datei | Zweck | Änderung |
|---|---|---|
| src/config/legal.ts | Zentrale Rechtskonfiguration | NEU |
| src/components/legal/legal-page.tsx | Shell für Rechtsseiten + Footer-Links | NEU |
| src/app/nutzungsbedingungen/… | Nutzungsbedingungen | NEU |
| src/app/verantwortungsvolle-nutzung/… | Verantwortungsvolle Nutzung | NEU |
| src/app/sicherheit/… | Sicherheit | NEU |
| src/app/unterstuetzen/… | Freiwilliger Unterstützungsbeitrag | NEU |
| src/app/organisationen/datenschutzvereinbarung/… | AVV-Entwurf | NEU |
| src/app/datenschutz/page.tsx | Datenschutzerklärung | NEUAUFBAU |
| src/app/impressum/page.tsx | Impressum | Claims korrigiert |
| src/app/page.tsx, spenden/…, admin/settings-tab | Landing/Spenden/Admin | Claims korrigiert |
| prisma/schema.prisma, prisma/migrate.js | LegalDocument/LegalAcceptance + Seeding | Erweitert |
| src/lib/legal.ts, legal-rules.ts, legal-content.ts | Consent-Logik | NEU |
| src/app/api/legal/*, api/admin/legal/* | Consent-/Admin-APIs | NEU |
| src/components/legal/legal-consent-gate.tsx | Re-Consent/Org-Bestätigung | NEU |
| src/app/konto/datenschutz/… , src/app/admin/legal/… | Konto- & Admin-Ansicht | NEU |
| src/app/register/page.tsx, api/auth/register | Zustimmung bei Registrierung | Erweitert |
| src/hooks/use-map-export.ts | Export-Prüfhinweis | Erweitert |
| src/components/legal/usage-notice-banner.tsx | Dashboard-Hinweis | NEU |
| src/components/map/measure-panel.tsx | Berechnungs-Hinweis | Erweitert |
| src/middleware.ts | noindex/no-store, /konto, /api/share | Erweitert |
| src/app/sitemap.ts | neue Seiten | Erweitert |
| docs/legal/*.md | Audit, Inventar, Subprozessoren, TOM, Retention, Incident, Lizenzen | NEU |
| vitest.config.ts, src/**/__tests__ | Unit-Tests | NEU |
## C. Datenbank
- **Neue Tabellen**: `legal_documents`, `legal_acceptances` (idempotent in `prisma/migrate.js`).
- **Neue Felder**: keine an bestehenden Tabellen (Consent liegt in eigenen Tabellen; Tenant-Felder
`privacyAccepted` etc. bestanden bereits).
- **Migration**: läuft idempotent beim Container-Start (bestehendes Muster). Dokumente werden geseedet/aktiviert.
- **Auswirkungen auf Bestand**: keine destruktiven Änderungen; bestehende Daten unberührt.
## D. Konfiguration (neue ENV, optional — Defaults sind sichtbare Platzhalter)
`LEGAL_OPERATOR_NAME`, `LEGAL_OPERATOR_FORM`, `LEGAL_POSTAL_ADDRESS`, `LEGAL_SUPPORT_EMAIL`,
`LEGAL_PRIVACY_EMAIL`, `LEGAL_SITE_DOMAIN`, `LEGAL_APP_DOMAIN`, `LEGAL_JURISDICTION`,
`LEGAL_HOSTING_PROVIDER`, `LEGAL_SERVER_LOCATION`, `LEGAL_BACKUP_RETENTION`.
## E. Manuelle Schritte nach Deployment
1. `LEGAL_*`-Angaben setzen (oder direkt in `src/config/legal.ts`).
2. `datenschutz@lageplan.ch` als überwachtes Postfach bestätigen.
3. Migration läuft automatisch; prüfen, dass die 5 Dokumente aktiv sind (`/admin/legal`).
4. Bestehende Org-Admins sehen beim nächsten Login die Organisationsbestätigung.
5. Stripe-/SMTP-DPA prüfen; Subprozessoren-Regionen ergänzen.
6. `/admin/legal` ggf. im Admin-Menü verlinken (aktuell direkte URL).
## F. Offene rechtliche Entscheidungen
Betreiberform/-adresse, Datenschutzkontakt, Serverstandorte (inkl. Backups/Logs), Unterauftragnehmer-
Regionen & DPAs, Aufbewahrungsfristen, Nutzungsrecht FKS/BABS-Symbole, Versicherung, AVV mit Feuerwehren,
juristische Schlussprüfung.
## G. Tests & Prüfung
- `npm test` (Vitest): **13 Tests grün** (Consent-Regeln, Re-Consent, 7 Org-Bestätigungen, Platzhalter,
keine erfundenen Subprozessoren).
- `tsc --noEmit`: nur vorbestehender `nodemailer`-Typhinweis (unabhängig).
- `next build`: erfolgreich (66 Seiten).
- Manuell zu prüfen (kein E2E-Framework, unverhältnismässig): Registrierung ohne Zustimmung blockiert,
Re-Consent-Dialog erscheint, Checkboxen einzeln/nicht vorausgewählt, Admin-Seiten nicht öffentlich.
## H. Nicht umgesetzt / bewusst als Folgeschritt
- **Plan-Freigabe-Workflow** (Entwurf/geprüft/freigegeben/archiviert, geprüft-von/-am, Prüf-Erinnerungen):
grosser neuer DB-+UI-Umbau; stattdessen Export-/Dashboard-/Berechnungs-Hinweise umgesetzt. Empfehlung als
eigenes Projekt.
- **MFA für Admins**, CSP verschärfen, Socket.io-CORS einschränken, geteilter Rate-Limit-Store: in
`technical-organizational-measures.md` als empfohlene Massnahmen dokumentiert.
- Cookie-Banner: unverändert (nur technisch notwendige Cookies; in Datenschutz dokumentiert).

View File

@@ -0,0 +1,23 @@
# Dateninventar / Verzeichnis der Bearbeitungstätigkeiten — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf, ersetzt keine juristische Prüfung.
Abgeleitet aus dem tatsächlichen Code-Stand. Offene Fristen/Standorte als Platzhalter `[…]`.
| Datenkategorie | Zweck | Betroffene | Herkunft | Speicherort | Empfänger/Drittanbieter | Auslandbekanntgabe | Aufbewahrung | Löschprozess | Schutzmassnahmen |
|---|---|---|---|---|---|---|---|---|---|
| Kontodaten (Name, E-Mail, Rolle) | Authentifizierung, Verwaltung | Benutzer | Eingabe bei Registrierung | PostgreSQL | — | `[SERVERSTANDORT]` | bis Kontolöschung | Selfservice/Antrag; Backups bis Zyklusende | Rollen/Mandanten, TLS, Zugriffskontrolle |
| Passwort (bcrypt-Hash) | Authentifizierung | Benutzer | Eingabe | PostgreSQL | — | `[SERVERSTANDORT]` | bis Kontolöschung | mit Konto | Einweg-Hash (bcrypt cost 12) |
| Einsatz-/Inhaltsdaten (Pläne, Journale, Zeichnungen, Koordinaten) | Kernfunktion | Org-Mitglieder, ggf. Dritte | Eingabe | PostgreSQL | — | `[SERVERSTANDORT]` | bis Löschung durch Org | Projekt-/Orglöschung; Backups | Mandantentrennung, Zugriffskontrolle |
| Hochgeladene Dateien (Logos, Planbilder, Symbole) | Darstellung | Org | Upload | MinIO | — | `[SERVERSTANDORT]` | bis Löschung | mit Projekt/Org | Zugriffsschutz, Validierung |
| Zustimmungsnachweise (Doc-Typ/Version/Zeitpunkt/Kontext, IP, User-Agent, Hash) | Nachweis Einwilligung/Vertrag (Beweiszweck) | Benutzer | System | PostgreSQL (Schweiz, Eigenbetrieb) | — | nein | Aufbewahrung zu Nachweiszwecken `[FRIST]` | revisionssicher, nicht überschrieben | Zugriffskontrolle; IP/User-Agent bewusst zu Beweiszwecken erfasst |
| Protokoll-/Sicherheitsdaten | Betrieb, Sicherheit | Benutzer | System | Server-Logs | — | `[SERVERSTANDORT]` | `[FRIST]` | rotierend | Minimierung sensibler Inhalte |
| Session-Cookie `auth-token` | Anmeldung | Benutzer | System | Client-Cookie | — | — | 24 h / 14 Tage | Ablauf/Logout | httpOnly, secure (prod), sameSite=lax |
| E-Mail-Versand (Verifizierung, Reset, Kontakt, Einladung) | Kommunikation | Benutzer/Empfänger | System/Eingabe | SMTP-Anbieter | E-Mail-Anbieter | `[ABHÄNGIG]` | beim Anbieter | — | TLS, nur bei Bedarf |
| Unterstützungsbeiträge (Betrag, Name/Nachricht, payment_intent) | Zahlungsabwicklung | Spender | Eingabe/Stripe | Stripe + `SystemSetting` | Stripe | ja (möglich) | `[FRIST]` | `[PROZESS]` | Zahlungsdaten nur bei Stripe |
| Karten-/Geo-Anfragen | Karten/Adresse/Wetter | Benutzer | Client | (extern) | OSM/Esri/swisstopo/Nominatim/Open-Meteo | möglich | keine Speicherung durch App | — | nur Kachel/Koordinaten |
| localStorage (Zeichnungen, Sync-Queue, Theme, Onboarding, letztes Projekt, Cookie-Hinweis) | Funktion/Offline | Benutzer | Client | Gerät des Nutzers | — | — | bis lokal gelöscht | Browser | verbleibt lokal |
## Offene Entscheidungen
- Konkrete Aufbewahrungsfristen (Logs, Zustimmungen, Zahlungsdaten) → siehe `retention-and-deletion.md`.
- Serverstandorte bestätigen.

View File

@@ -0,0 +1,44 @@
# Incident-Response — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf. **Manueller Entscheidungsprozess** — es erfolgt KEINE
automatische Meldung an Behörden. Meldepflichten werden im Einzelfall geprüft.
## Kontakte (auszufüllen)
- Verantwortlich (Betreiber): `[NAME]` · `[KONTAKT]`
- Datenschutzkontakt: siehe `src/config/legal.ts` (`privacyContactEmail`)
- Technischer Notfallkontakt: `[NAME/TELEFON]`
- Zuständige Aufsichtsbehörde (CH): EDÖB — `[LINK/KONTAKT]`
## Ablauf
1. **Erkennung** — Quelle (Monitoring, Meldung, Nutzerhinweis), Zeitpunkt und Symptome festhalten.
2. **Interne Eskalation** — Verantwortliche informieren; Vorfall im Vorfallprotokoll eröffnen.
3. **Eindämmung** — betroffene Systeme/Zugänge isolieren, ggf. Dienste temporär sperren.
4. **Beweissicherung** — Logs/Artefakte sichern, bevor Änderungen erfolgen.
5. **Bewertung betroffener Daten** — welche Datenkategorien/Personen sind betroffen?
6. **Risikobewertung** — Eintritts-/Schadenswahrscheinlichkeit für Betroffene.
7. **Information betroffener Organisationen** — ohne unangemessene Verzögerung, sachlich.
8. **Prüfung Behördenmeldung** — Meldepflicht (z.B. EDÖB) im Einzelfall prüfen und **manuell** entscheiden/dokumentieren.
9. **Passwort-/Schlüsselrotation** — Secrets, Tokens, ggf. Nutzer-Passwörter rotieren.
10. **Wiederherstellung** — aus sauberem Stand/Backup; Integrität prüfen.
11. **Nachbearbeitung** — Ursachenanalyse, Massnahmen, Lessons Learned.
## Vorfallprotokoll (Vorlage)
| Feld | Inhalt |
|---|---|
| Vorfall-ID / Datum | |
| Entdeckt durch / wann | |
| Beschreibung | |
| Betroffene Daten/Personen | |
| Sofortmassnahmen | |
| Risikobewertung | |
| Information an Organisationen (wann/wie) | |
| Behördenmeldung (ja/nein, Begründung) | |
| Rotation durchgeführt | |
| Wiederherstellung | |
| Abschluss / Massnahmen | |
## Vom Betreiber zu treffende Entscheidungen
- Verbindliche Kontaktliste und Erreichbarkeit.
- Melde-Schwellen und interne Fristen.
- Aufbewahrung des Vorfallprotokolls.

View File

@@ -0,0 +1,45 @@
# Lizenzen und Attributionen — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf. Eine Quellenangabe ersetzt NICHT automatisch eine
erforderliche Nutzungslizenz. Offene Freigabefragen sind markiert.
## Karten & Geodaten
| Quelle | Nutzung | Lizenz/Bedingungen | Attribution | Offene Frage |
|---|---|---|---|---|
| OpenStreetMap | Basiskarte, Nominatim | ODbL (Daten) | "© OpenStreetMap-Mitwirkende" (vorhanden im Impressum/Karte) | Tile-Usage-Policy einhalten (Fair Use) |
| Esri World Imagery | Satellitenlayer | Esri-Nutzungsbedingungen | "Esri World Imagery" | ⚠️ kommerzielle/Nutzungsbedingungen prüfen |
| swisstopo (geo.admin.ch) | Landeskarte | swisstopo-Bedingungen | "© swisstopo" empfohlen | ⚠️ Attribution/Bedingungen bestätigen |
| Open-Meteo | Wind/Höhe | Open-Meteo Terms (frei, Attribution empfohlen) | "Open-Meteo" | Attribution ergänzen? |
| Open-Elevation | Höhe (Fallback) | offen/community | — | ⚠️ Bedingungen prüfen |
## Symbole / Icons
| Quelle | Nutzung | Lizenz | Attribution | Offene Frage |
|---|---|---|---|---|
| FKS/BABS taktische Zeichen | Einsatzsymbole | `[NUTZUNGSRECHT ZU KLÄREN]` | "Bildquelle Feuerwehr Koordination Schweiz FKS" (vorhanden) | ⚠️ **Nutzungsrecht/Freigabe FKS bestätigen** |
| lucide-react | UI-Icons | ISC | — (im Code) | ok |
## Schriftarten
| Quelle | Lizenz | Hinweis |
|---|---|---|
| Barlow (Google Fonts, via next/font) | SIL Open Font License 1.1 | build-time self-hosted, kein Laufzeit-Request zu Google |
## Wesentliche Bibliotheken (Auszug)
| Paket | Lizenz |
|---|---|
| next / react | MIT |
| maplibre-gl | BSD-3-Clause |
| jspdf | MIT (+ Teile Apache-2.0) |
| qrcode | MIT |
| stripe (SDK) | MIT |
| bcryptjs, jose, zod, socket.io | MIT |
| @prisma/client, prisma | Apache-2.0 |
## Bereits umgesetzte sichtbare Attributionen
- Karten-Attribution "© OpenStreetMap contributors | MapLibre" auf der Karte.
- "Taktische Symbole: Bildquelle Feuerwehr Koordination Schweiz FKS" im Footer.
- Kartendaten-Hinweis im Impressum (OpenStreetMap, Nominatim).
## Offene Freigabefragen (Betreiber)
1. **FKS/BABS-Symbole**: Nutzungsrecht/Freigabe für die konkrete Verwendung schriftlich bestätigen.
2. Esri/swisstopo-Nutzungsbedingungen für den vorgesehenen (auch nicht-kommerziellen) Einsatz prüfen.
3. Vollständige Lizenzliste (z.B. `license-checker`) generieren und Drittlizenzen bündeln, falls verteilt.

View File

@@ -0,0 +1,37 @@
# Aufbewahrungs- und Löschkonzept — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf. **Keine Fristen erfunden** — offene Fristen als `[FRIST — zu
entscheiden]`. Der Betreiber muss die markierten Entscheidungen treffen.
| Datenkategorie | Aufbewahrung | Löschung / Prozess |
|---|---|---|
| Aktive Benutzerkonten | solange Konto besteht | Selfservice/Antrag; kaskadierende Löschung zugehöriger Daten |
| Deaktivierte Konten (`isActive=false`) | `[FRIST]` | `[PROZESS — automatische Löschung nach X?]` |
| Organisationen (Tenant) | solange aktiv | Löschung durch Org-Admin (Einstellungen) nach Berechtigungsprüfung |
| Pläne / Projekte | bis Löschung durch Org | mit Projekt-/Orglöschung (Cascade) |
| Dateiuploads (MinIO) | bis Löschung des Bezugsobjekts | mit Projekt/Org/Icon |
| Audit-/Aktionslogs (falls geführt) | `[FRIST]` | `[PROZESS]` |
| Sicherheits-/Server-Logs | `[FRIST]` | Rotation `[KONFIG]` |
| Einladungen | `[FRIST]` | nach Annahme/Ablauf |
| Passwort-Reset-Tokens | Ablauf (`resetTokenExpiry`) | mit Verwendung/Ablauf |
| E-Mail-Verifizierungs-Tokens | bis Verifizierung | mit Verifizierung |
| E-Mails (beim SMTP-Anbieter) | beim Anbieter | `[ABHÄNGIG]` |
| Zahlungsinformationen (Stripe + `SystemSetting/donations`) | `[FRIST]` (ggf. gesetzliche Aufbewahrung) | `[PROZESS]`; Zahlungsdaten liegen bei Stripe |
| Supportanfragen (Kontaktformular) | `[FRIST]` | `[PROZESS]` |
| Backups | `[BACKUP-AUFBEWAHRUNGSFRIST]` | mit Ablauf des Backup-Zyklus |
| Zustimmungshistorie (LegalAcceptance) | zu Nachweiszwecken `[FRIST]` | revisionssicher, i.d.R. nicht gelöscht solange Konto besteht |
## Grundsätze
- Produktivdaten werden nach Abschluss des Löschvorgangs entfernt.
- Daten in Sicherungskopien können bis zum Ablauf des regulären Backup-Zyklus fortbestehen.
- Organisationsrelevante Daten werden nicht ohne Berechtigungsprüfung sofort gelöscht.
- Betroffenenrechte (Auskunft, Berichtigung, Löschung, Export) über `/konto/datenschutz` bzw.
Datenschutzkontakt; Eingang wird bestätigt.
## Vom Betreiber zu treffende Entscheidungen
1. Aufbewahrungsfrist deaktivierter Konten und automatischer Löschlauf?
2. Log-Aufbewahrung (Server-/Sicherheitslogs) und Rotation?
3. Aufbewahrung Zahlungs-/Spendendaten (gesetzliche Pflichten?).
4. Backup-Aufbewahrungsfrist (`LEGAL_BACKUP_RETENTION`).
5. Aufbewahrung Zustimmungsnachweise nach Kontolöschung.
6. Support-/Kontaktanfragen-Frist.

View File

@@ -0,0 +1,33 @@
# Unterauftragnehmer / Subprozessoren — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf, ersetzt keine juristische Prüfung.
Nur tatsächlich im Code verwendete bzw. optional konfigurierbare Dienste. Nicht verwendete Dienste
sind NICHT aufgeführt. Regionen/Verträge, die nicht sicher bekannt sind, tragen Platzhalter `[…]`.
| Anbieter | Zweck | Verarbeitete Daten | Sitz / Region | Datenschutzhinweis | Auslandbearbeitung | Vertragsstatus |
|---|---|---|---|---|---|---|
| OpenStreetMap Foundation (Tiles) | Basiskarte | Kachel-Koordinaten, IP (Client) | `[REGION — UK/EU]` | osmfoundation.org/wiki/Privacy_Policy | möglich | `[ZU PRÜFEN]` |
| Esri (ArcGIS World Imagery) | Satellitenbilder | Kachel-Koordinaten, IP (Client) | `[REGION — USA/global]` | esri.com/legal | wahrscheinlich | `[ZU PRÜFEN]` |
| swisstopo (geo.admin.ch) | Schweizer Landeskarte | Kachel-Koordinaten, IP (Client) | Schweiz | geo.admin.ch | nein | `[ZU PRÜFEN]` |
| Nominatim (OSM) | Adress-/Reverse-Geocoding | Suchbegriff/GPS, IP (Client) | `[REGION — OSMF]` | osmfoundation.org | möglich | `[ZU PRÜFEN]` |
| Open-Meteo | Wind-/Höhendaten | Koordinaten, IP (Client) | `[REGION — EU/DE]` | open-meteo.com/en/terms | möglich | `[ZU PRÜFEN]` |
| Open-Elevation | Höhendaten (Fallback) | Koordinaten, IP (Client) | `[REGION]` | open-elevation.com | möglich | `[ZU PRÜFEN]` |
| Stripe | Zahlungsabwicklung (freiwillige Beiträge) | Betrag, freiwilliger Name/Nachricht, Zahlungsdaten (direkt bei Stripe) | `[REGION — USA/EU]` | stripe.com/privacy | ja (USA möglich) | `[DPA PRÜFEN]` · nur wenn konfiguriert |
| SMTP-/E-Mail-Anbieter (konfigurierbar) | System-E-Mails | Empfängeradresse, Mailinhalt | `[ABHÄNGIG VOM ANBIETER]` | `[ANBIETER]` | abhängig | `[ZU PRÜFEN]` · nur wenn konfiguriert |
| MinIO (Objektspeicher, selbst gehostet) | Datei-/Bild-Uploads | Pläne, Logos, Symbole | `[SERVERSTANDORT]` | — (Eigenbetrieb) | `[ZU BESTÄTIGEN]` | Eigenbetrieb |
| PostgreSQL (Datenbank, selbst gehostet) | Primäre Datenhaltung | Konto-, Org-, Plan-, Journaldaten | `[SERVERSTANDORT]` | — (Eigenbetrieb) | `[ZU BESTÄTIGEN]` | Eigenbetrieb |
## Hinweise
- **Client-seitige Dienste** (Tiles, Nominatim, Open-Meteo/Elevation): werden direkt aus dem Browser
des Nutzers aufgerufen; dabei kann die IP-Adresse an den Anbieter gelangen.
- **Stripe** und **SMTP** sind nur aktiv, wenn in den Systemeinstellungen (DB `SystemSetting`) konfiguriert.
- **MapTiler / Mapbox / Google Maps**: NICHT genutzt (MapTiler nur in der CSP-Allowlist vorbereitet).
- **Analytics / Error-Tracking (Sentry, GA, Plausible, PostHog)**: NICHT vorhanden.
## Offene Entscheidungen des Betreibers
- Serverstandort für PostgreSQL, MinIO, Backups und Logs abschliessend bestätigen.
- DPA/AVV mit Stripe und dem gewählten E-Mail-Anbieter abschliessen und Status hier eintragen.
- Verarbeitungsregionen der Kartendienste konkretisieren.

View File

@@ -0,0 +1,60 @@
# Technische und organisatorische Massnahmen (TOM) — Lageplan
Stand: 2026-07-23 · sorgfältiger Entwurf. Ist-Zustand aus dem Code; offene Punkte klar markiert.
Legende: ✅ vorhanden · ⚠️ teilweise / zu bestätigen · ❌ offen / nicht umgesetzt.
## Zugriff & Berechtigungen
- ✅ Mandantentrennung je Organisation (Tenant), `getProjectWithTenantCheck`.
- ✅ Rollenkonzept: SERVER_ADMIN, TENANT_ADMIN, OPERATOR, VIEWER.
- ✅ MFA/Zwei-Faktor verfügbar: TOTP (Authenticator-App) + WebAuthn (YubiKey/Passkeys), freiwillig
pro Benutzer; Admin kann MFA zurücksetzen. Backup-Codes als Fallback. (SMS bewusst nicht umgesetzt.)
- ✅ Admin-APIs serverseitig mit `isAdmin`/`isServerAdmin` geschützt.
## Authentifizierung & Sessions
- ✅ Passwort-Hashing: bcrypt (cost 12), nie Klartext.
- ✅ JWT-Session (jose, HS256), Cookie `auth-token` httpOnly, secure (prod), sameSite=lax, 24 h/14 Tage.
- ✅ Passwort-Reset mit kryptographischem Token (32 Bytes), Ablauf.
- ✅ E-Mail-Verifizierung.
## Transport & Web-Härtung
- ✅ HTTPS/TLS; HSTS (max-age 2 Jahre, preload).
- ✅ Sicherheits-Header: CSP (mit `frame-ancestors 'self'`), X-Content-Type-Options nosniff,
Referrer-Policy, Permissions-Policy, COOP, X-XSS-Protection.
- ⚠️ CSP enthält `unsafe-inline`/`unsafe-eval` (für MapLibre gelockert) → langfristig verschärfen.
- ✅ Private Routen (/app,/admin,/settings,/konto): `X-Robots-Tag: noindex` + `Cache-Control: no-store` (Middleware).
- ⚠️ Socket.io-Server (`server-custom.js`): CORS `origin:'*'`**einschränken empfohlen**.
## Eingaben & Uploads
- ✅ Rate-Limiting: Login, Registrierung, Passwort-Reset, Verifizierung, Kontakt, Konto-Löschung, Share-PIN.
- ⚠️ Rate-Limiter ist In-Memory (pro Instanz) → bei Mehr-Instanz-Betrieb geteilten Store nutzen.
- ⚠️ Upload-Validierung (MIME/Grösse): vorhanden für Icons/Logos/Planbilder → **Umfang bestätigen**.
- ✅ Zugriffsschutz auf Dateien (serve-Routen mit Tenant-Check).
## Speicherung & Verschlüsselung
- ✅ Übertragung verschlüsselt (TLS).
- ⚠️ Verschlüsselung ruhender Daten (DB/MinIO/Backups): `[ZU BESTÄTIGEN]`.
- ⚠️ Secrets-Verwaltung: ENV (`NEXTAUTH_SECRET`, MinIO) + DB (`SystemSetting`: SMTP/Stripe) → **Absicherung der DB-Secrets bestätigen**.
## Backup & Wiederherstellung
- ⚠️ Backups: `[VORHANDEN? FREQUENZ? AUFBEWAHRUNG?]` (siehe `docs/backup-und-seeding.md`).
- ❌ Restore-Tests: `[DOKUMENTIEREN]`.
## Logging & Monitoring
- ✅ Server-Logs (console). ⚠️ Vermeidung sensibler Daten in Logs → **Review empfohlen**.
- ❌ Zentrales Monitoring/Alerting: `[OFFEN]`.
## Betrieb & Prozesse
- ✅ Idempotente Migrationen (`prisma/migrate.js`) beim Container-Start.
- ⚠️ Dependency-/Patch-Management: `[PROZESS DOKUMENTIEREN]`.
- ⚠️ Trennung Entwicklung/Test/Produktion: `[BESTÄTIGEN]`; Umgang mit Testdaten `[BESTÄTIGEN]`.
- ✅ Löschkonzept/Datenexport: siehe `retention-and-deletion.md`; Betroffenenrechte via `/konto/datenschutz`.
- ✅ Berechtigungsentzug: Benutzer sperren (`isActive`), aus Organisation entfernen.
## Direkt umgesetzte risikoarme Verbesserungen (dieses Update)
- Private Routen auf `noindex`/`no-store` gesetzt.
- Rechtliche Prüf-Hinweise in Exporten und Berechnungen.
- Revisionssichere Zustimmungsprotokollierung ohne roh-IP.
## Empfohlene, noch offene Massnahmen
- MFA für Admins; CSP verschärfen; Socket.io-CORS einschränken; geteilter Rate-Limit-Store;
Verschlüsselung ruhender Daten + Backups bestätigen; Restore-Tests; Monitoring.

View File

@@ -0,0 +1,46 @@
# Website Claims Audit — Lageplan
Stand: 2026-07-23 · Version 1.0 (sorgfältiger Entwurf, ersetzt keine juristische Prüfung)
Prüfung aller werblichen / absoluten Aussagen im Repository und deren Korrektur zu sachlichen,
nachweisbaren Formulierungen. Fundstellen beziehen sich auf den Stand vor der Korrektur.
| # | Bisherige Aussage | Fundstelle (vorher) | Risiko / Widerspruch | Neue Formulierung | Geänderte Datei |
|---|---|---|---|---|---|
| 1 | „100% · Swiss Hosted · Server in der Schweiz" | `src/app/page.tsx:241` | Serverstandort nicht für alle Subprozessoren (Stripe, SMTP, globale Kartendienste) belegt; absolutes 100% | „CH · Swiss Made · In der Schweiz entwickelt" | `src/app/page.tsx` |
| 2 | „24/7 · Verfügbar" | `src/app/page.tsx:243` | Verfügbarkeitszusage ohne SLA | „Web · Überall nutzbar · Browser, Tablet, Smartphone" | `src/app/page.tsx` |
| 3 | „Die Applikation ist DSG- und DSGVO-konform." (FAQ + JSON-LD) | `src/app/page.tsx:87,458` | Unbelegte pauschale Konformitätszusage | „Die eingesetzten Dienstleister und Speicherorte sind in der Datenschutzerklärung transparent aufgeführt. Lageplan orientiert sich am Schweizer Datenschutzgesetz (DSG)." | `src/app/page.tsx` |
| 4 | „Sicher & Schweiz … DSG/DSGVO-konform." (Feature-Karte) | `src/app/page.tsx:361` | Absolute Sicherheits-/Konformitätszusage | „Sicherheit & Datenschutz … Angemessene technische und organisatorische Schutzmassnahmen — Details in der Datenschutzerklärung." | `src/app/page.tsx` |
| 5 | „Lückenlose Protokollierung aller Aktionen" | `src/app/page.tsx:360` | „lückenlos" = absolute Zusage | „Protokollierung von Aktionen mit Zeitstempel für bessere Nachvollziehbarkeit." | `src/app/page.tsx` |
| 6 | „Nichts. Lageplan ist kostenlos …" (FAQ + JSON-LD) | `src/app/page.tsx:55,442` | Impliziert dauerhafte Kostenlosigkeit | „… grundsätzlich kostenlos … Ein Anspruch darauf, dass alle Funktionen dauerhaft kostenlos bleiben, besteht nicht." | `src/app/page.tsx` |
| 7 | „100% deiner Spende fliesst direkt …" | `src/app/page.tsx:588`, `src/app/spenden/page.tsx:252` | Ignoriert Stripe-Gebühren; absolutes 100% | „Beiträge werden nach Abzug allfälliger Zahlungsgebühren für Hosting, Infrastruktur und Weiterentwicklung eingesetzt." | `src/app/page.tsx`, `src/app/spenden/page.tsx` |
| 8 | „Kein Unternehmen, keine Investoren …" | `src/app/page.tsx:589`, `src/app/spenden/page.tsx:253` | Betreiberform ist rechtlich noch festzulegen; potenziell irreführend | „Ein Projekt aus der Feuerwehr-Praxis — entwickelt in der Freizeit." | `src/app/page.tsx`, `src/app/spenden/page.tsx` |
| 9 | „Lageplan ist und bleibt kostenlos." | `src/app/page.tsx:551`, `src/app/spenden/page.tsx:239` | „bleibt" = dauerhafte Zusage | „Lageplan wird grundsätzlich kostenlos angeboten." | `src/app/page.tsx`, `src/app/spenden/page.tsx` |
| 10 | „Zahlung sicher via Stripe" | `src/app/page.tsx:574`, `src/app/spenden/page.tsx:226` | „sicher" = absolute Zusage | „Zahlung über Stripe" | `src/app/page.tsx`, `src/app/spenden/page.tsx` |
| 11 | „Server-Hosting in der Schweiz (monatliche Kosten)" | `src/app/spenden/page.tsx:99` | Serverstandort nicht abschliessend belegt | „Server-Hosting und Infrastruktur (monatliche Kosten)" | `src/app/spenden/page.tsx` |
| 12 | „Jeder Franken fliesst direkt in Serverkosten …" | `src/app/spenden/page.tsx:244` | Ignoriert Gebühren | „Beiträge helfen, Hosting, Infrastruktur und Weiterentwicklung mitzufinanzieren." | `src/app/spenden/page.tsx` |
| 13 | „weder an Dritte verkauft noch weitergegeben" | `src/app/impressum/page.tsx:108` | Absolute „keine Weitergabe"; widerspricht Einsatz von Dienstleistern | „… nicht an Dritte verkauft. Für den technischen Betrieb können sorgfältig ausgewählte … Dienstleister eingesetzt werden. Einzelheiten enthält die Datenschutzerklärung." | `src/app/impressum/page.tsx` |
| 14 | „Einsatzdaten … werden verschlüsselt gespeichert" | `src/app/impressum/page.tsx:116` | „verschlüsselt gespeichert" technisch nicht belegt (Ruhedaten) | Zugriffs-/Mandantenkonzept + „Übertragung erfolgt über HTTPS/TLS"; Passwörter „gehasht (bcrypt)" | `src/app/impressum/page.tsx` |
| 15 | „Passwort (verschlüsselt gespeichert)" | `src/app/datenschutz/page.tsx:47` | Falsch: Passwörter werden gehasht, nicht verschlüsselt | „Passwort wird mit einem geeigneten Einwegverfahren (bcrypt) gehasht" | `src/app/datenschutz/page.tsx` (Neuaufbau) |
| 16 | „Alle Daten … auf Servern in der Schweiz … keine Übermittlung ins Ausland" | `src/app/datenschutz/page.tsx:105-106` | Widerspricht Stripe/SMTP/Kartendiensten; absolute Auslandsaussage | Ziffer 6 „Bekanntgabe ins Ausland": Auslandbearbeitung je nach Dienstleister möglich | `src/app/datenschutz/page.tsx` (Neuaufbau) |
| 17 | „Personenbezogene Daten werden nicht an Dritte weitergegeben" | `src/app/datenschutz/page.tsx:91,164` | Absolute „keine Weitergabe" | Dienstleisterliste (Subprozessoren) + „nicht zu Werbezwecken verkauft" | `src/app/datenschutz/page.tsx` (Neuaufbau) |
| 18 | „Gemäss DSG und DSGVO haben Sie folgende Rechte" | `src/app/datenschutz/page.tsx:144` | Pauschale DSGVO-Anwendbarkeit unbelegt | „Im Rahmen des anwendbaren Rechts …" + Beschwerde bei EDÖB | `src/app/datenschutz/page.tsx` (Neuaufbau) |
| 19 | „Bei der Löschung werden sämtliche Daten unwiderruflich entfernt" | `src/app/datenschutz/page.tsx:128` | Ignoriert Backups | „Produktivdaten werden nach Abschluss des Löschvorgangs entfernt. Daten in Sicherungskopien können bis zum Ablauf des Backup-Zyklus fortbestehen." | `src/app/datenschutz/page.tsx` (Neuaufbau) |
| 20 | „Passwörter werden verschlüsselt in der Datenbank gespeichert." | `src/components/admin/settings-tab.tsx:242` | Falsch: gehasht, nicht verschlüsselt | „… mit einem Einwegverfahren (bcrypt) gehasht …" | `src/components/admin/settings-tab.tsx` |
## Nicht beanstandet (Kontext geprüft)
- Zahlreiche `100%` in `rapport-pdf.tsx`, `measure-panel.tsx`, `map-view.tsx`, E-Mail-Templates → CSS-Breitenangaben, keine Werbeaussage.
- „sicher" in `README.md`, `docs/*`, `jwt-secret.ts`, `modules.ts` → technische Kommentare, nicht öffentlich.
- „SSL-Zertifikate" (Substantiv) → keine „zertifiziert"-Behauptung.
## Nicht gefunden (im gesamten Repo)
`garantiert`, `fehlerfrei`, `gemeinnützig`, `steuerlich`, `rechtssicher`, `zertifiziert`,
„immer verfügbar", „kostenlos für immer" — keine Treffer.
## Noch offen (Betreiber-Entscheid)
- Formulierung „In der Schweiz entwickelt": Sobald der Serverstandort für alle Subprozessoren/Backups
bestätigt ist, kann ggf. eine präzisere Hosting-Aussage getroffen werden.
- „Purepixel / Kingstickers Ziberi" (Impressum-Copyright): rechtsverbindliche Betreiberbezeichnung bestätigen.

View File

@@ -0,0 +1,273 @@
# Roadmap — Feedback Fabian (Mai 2026)
> **Quelle**: User-Feedback von Fabian, Führungsoffizier / Übungsleiter Feuerwehr.
> **Erhalten**: 20.05.2026
> **Status**: Geplant, noch nicht umgesetzt.
Dieses Dokument ist die strukturierte Roadmap aus Fabians Feedback. Jede KI / jeder Entwickler der an Lageplan arbeitet, soll diese Datei vor Beginn lesen, um Kontext und Priorisierung zu kennen.
---
## Originalfeedback (Zusammenfassung)
Fabian sucht eine Lösung um **Einsätze und Übungen einfach zu skizzieren und zu krokieren**. Lobt die App, gibt aber konkrete Verbesserungsvorschläge aus Anwendersicht:
1. **Einsatz vs. Übung unterscheiden** — bei Übungen kein Journal nötig, dafür Übungsziele/Auswertung
2. **Symbol-Bibliothek erweitern** — Warteraum, Rettungsachse, taktische Zeichen
3. **Symbol-Schönheitsfehler** — Treppe/Eingang mit Umriss, Absperrung
4. **Stockwerke/Geschosse** — eigene Werte eintragen
5. **Symbol-Kategorien aufräumen** — Motorspritze unter Organisation statt Geräte; Hydrant/Leitern uneinheitlich
6. **Linien mit Typ** — nach Zeichnen wählen: Rettungsachse (gestrichelt + R), Leitung (blau), Schlauch, etc.
7. **Multi-User Live** — eine Person zeichnet Karte, andere schreibt Journal gleichzeitig
8. **Rapport-/Lageansicht** — separate Sicht mit Pendenzen, aktueller Stand, wichtige Punkte für Führungsunterstützung (FU), darstellbar auf grossem Bildschirm; nicht alle Journaleinträge sichtbar für alle
---
## Phasen-Plan
### Phase 1 — Symbol-Architektur Redesign (3-4 Wochen) ⭐⭐
**Strategischer Umbau** statt nur Kategorien aufräumen. Begründung:
- Fabians Feedback zeigt, dass die aktuelle Kategorisierung uneinheitlich ist
- Symbol-Bibliothek ist zu starr — andere Anwender (THW, Sanität, ausländische Wehren) brauchen andere Symbole
- Mandantenfähigkeit ist zentrales Versprechen → muss auch für Symbole konsequent durchgezogen werden
#### 1.1 Neue Symbol-Architektur: Template-Import + 100% mandantenspezifisch
**Konzept**: Mandant startet mit leerer Bibliothek, importiert beim Onboarding (oder jederzeit) **kuratierte Vorlagen-Pakete** (Feuerwehr CH, THW, Sanität…). Importierte Symbole werden vollständig eigene Mandanten-Daten — umbenennbar, löschbar, kategorisierbar, ergänzbar.
**Datenmodell**:
```
TenantCategory (per-tenant, frei definierbar)
- id, tenantId, name, sortOrder, icon
TenantSymbol (per-tenant, ehemals "Meine Symbole")
- id, tenantId, categoryId → TenantCategory
- name, svgPath, sortOrder, customName
SymbolTemplate (global, read-only)
- id, packageId ("feuerwehr-ch", "thw", "sanitaet")
- categoryName, name, svgPath, tags
```
**Tasks**:
- [x] Schema-Migration: `TenantCategory` neu, `TenantSymbol.categoryId` ergänzen, `SymbolTemplate` erstellen
- [x] Migration SQL + idempotente Raw-SQL-Migration (`prisma/migrate.js`)
- [x] Seed-Skript: `public/signaturen/*.svg``SymbolTemplate`
- [x] Migration bestehender Tenants: `TenantSymbol.name`, `svgPath`, `categoryId` befüllen
- [ ] **Auto-Import des `feuerwehr-ch` Pakets beim ersten Login** (oder bei leerer Bibliothek)
- [ ] API: CRUD für `TenantCategory` (`GET/POST/PATCH/DELETE /api/tenant/categories`)
- [ ] API: `GET /api/templates` — listet verfügbare Pakete
- [ ] API: `POST /api/templates/import` — importiert ausgewählte Symbole als TenantSymbols
- [ ] API: `TenantSymbol` erweitern — Upload-Endpoint, Kategorie-Zuordnung, gruppierte Liste
- **Files**: `prisma/schema.prisma`, `src/app/api/tenant/categories/`, `src/app/api/tenant/symbols/`, `src/app/api/templates/`
#### 1.2 UX: Symbol-Manager im Admin
**Sidebar / Symbol-Verwaltung**:
```
┌─ Meine Symbole ─────────────┐
│ ┌ Fahrzeuge ─────────┐ │
│ │ 🚒 TLF │ │
│ │ 🚒 RW │ │
│ │ + Symbol │ │
│ └────────────────────┘ │
│ ┌ Wasser ────────────┐ │
│ │ 🟦 Hydrant │ │
│ └────────────────────┘ │
│ [+ Kategorie] │
│ [📦 Vorlagen importieren] │
└─────────────────────────────┘
```
**Tasks**:
- [ ] Admin-Tab: Kategorien anlegen/umbenennen/sortieren (Drag & Drop)
- [ ] Symbole pro Kategorie verwalten (Drag & Drop zwischen Kategorien)
- [ ] Eigene SVG-Uploads direkt einer Kategorie zuordnen
- [ ] Import-Dialog: Pakete-Auswahl mit Vorschau, granulare Symbol-Auswahl
- [ ] Mehrfach-Import desselben Symbols erlaubt (z.B. "TLF rot" + "TLF blau" auf gleicher SVG-Basis)
- **Files**: `src/components/admin/icons-tab.tsx` (umbauen), neuer `import-templates-dialog.tsx`
#### 1.3 Vorlagen-Pakete kuratieren
Aus dem aktuellen Symbol-Bestand werden die ersten Pakete:
- [ ] 📦 **Feuerwehr Schweiz** — taktische Zeichen CH (aktueller Bestand, sauber kategorisiert)
- [ ] 📦 **Sanität / Rettungsdienst** — falls Symbole vorhanden, sonst Phase 2
- [ ] 📦 **Polizei / Verkehr** — als Stub für später
- [ ] Innerhalb der Pakete: **fehlende taktische Zeichen ergänzen**:
- Warteraum
- Rettungsachse
- Sammelplatz
- Verletztennest / Eingang Verletzter
- Einsatzleitung (EL) / Kommandoposten
- Bereitstellungsraum
- [ ] **Symbol-Schönheitsfehler im Paket** beheben:
- Treppe — Umriss entfernen
- Eingang — Umriss entfernen
- Absperrung — Seitenlinien aufräumen
- **Files**: `prisma/seed/symbol-templates.ts`, `public/icons/`
#### 1.4 Stockwerke/Geschosse editierbar
- [ ] Symbol-Property: `instanceLabel` (Freitext, pro Symbol-Instanz auf der Karte)
- [ ] Bei Gebäude-Symbolen: optionales Textfeld für Geschosszahl (z.B. "EG+3")
- [ ] Anzeige als Badge auf dem Symbol
- [ ] Editierbar via Doppelklick oder Sidebar
- **Files**: `src/types/`, `map-view.tsx` Symbol-Renderer, ggf. DrawFeature.properties erweitern
---
### Phase 1.5 — Quick Polish (parallel oder danach, 1 Woche) 🔥
Kleinkram der nicht in den Architektur-Umbau passt aber wichtig ist.
- [ ] Default-Linientyp / -farbe pro Mandant konfigurierbar
- [ ] Symbol-Suche im Sidebar (Volltext über Name, Tags, Kategorie)
- [ ] Häufig-benutzte Symbole oben anzeigen (Recent / Favoriten)
---
### Phase 2 — Übungsmodus (2-3 Wochen) ⭐
Neuer Projekt-Typ, eigene Logik.
#### 2.1 Projekt-Typ Auswahl
- [ ] Bei "Neues Projekt": Auswahl `Einsatz | Übung`
- [ ] DB-Schema: `Project.type: 'einsatz' | 'uebung'`
- [ ] UI-Anpassung in `app/page.tsx`
#### 2.2 Übungs-Metadaten
- [ ] Felder: Übungstitel, Datum, **Übungsziele (Liste)**
- [ ] Übungsleiter, Teilnehmer (optional)
- [ ] Eigene Sidebar-Sektion für Übungsdaten
#### 2.3 Journal ausblenden bei Übung
- [ ] Bei `type === 'uebung'`: Journal-Tab → "Übungsauswertung"
- [ ] Karte bleibt voll funktionsfähig
- [ ] Krokierung kann **im Voraus** erstellt und mit Übungsleitern geteilt werden (bestehender Share-Link)
#### 2.4 Übungsauswertung
- [ ] Checkliste **Übungsziele** mit Status: `erreicht | teilweise | nicht erreicht`
- [ ] Notizen / Erkenntnisse pro Ziel
- [ ] Gesamtbewertung / Erkenntnisse
- [ ] **PDF-Export** für Debriefing (analog Rapport)
- **Files**: neue Komponente `uebungsauswertung-tab.tsx`, neuer PDF-Renderer
---
### Phase 3 — Linien mit Typ (1 Woche) ⭐
Smart Lines mit vordefinierten Stilen.
#### 3.1 Linientypen definieren
- [ ] **Schlauch** (default, aktuelle Linie)
- [ ] **Rettungsachse** — gestrichelt, Label "R"
- [ ] **Leitung** — blau
- [ ] **Absperrung** — rot gestrichelt
- [ ] **Frei** — Standard ohne Vorlage
#### 3.2 UX
- [ ] Nach Zeichnen einer Linie: kleines Popup "Was ist das?"
- [ ] Auswahl per Klick → Linie wird automatisch gestylt
- [ ] In Sidebar pro Linie nachträglich änderbar (Dropdown)
- [ ] Tastatur-Shortcut für Typ-Wechsel?
#### 3.3 Daten-Modell
- [ ] `DrawFeature` erweitern: `lineCategory: 'hose' | 'rescue' | 'pipe' | 'barrier' | 'free'`
- [ ] Default-Stile als Konstante (Farbe, Strich, Label)
- **Files**: `src/types/`, `tool-store.ts`, `map-view.tsx`
---
### Phase 4 — Rapport-/Lageansicht (Führungsunterstützung) (2-3 Wochen) ⭐
Eigenes Modul, hoher Mehrwert für FU.
#### 4.1 Sichtbarkeits-Stufen pro Journaleintrag
- [ ] DB: `JournalEntry.visibility: 'intern' | 'rapport' | 'public'`
- [ ] UI: Dropdown beim Erstellen / Editieren
- [ ] Migration: bestehende Einträge → `intern` (sicher)
#### 4.2 Rapport-Modus (Display)
- [ ] Neuer Tab/Modus: **Rapport-Ansicht**
- [ ] Sektionen:
- **Aktuelle Lage** (Freitext, prominentes Feld)
- **Pendenzen** (Liste mit Status: offen/erledigt)
- **Wichtige Punkte / Befehle**
- **Mittel / Personal** (aktueller Stand)
- [ ] Grosse Schrift, kontrastreich (Beamer-tauglich)
- [ ] Auto-Refresh via Socket.IO
#### 4.3 Display-Sharing
- [ ] Read-only Token-Link (wie bestehende Rapport-URL)
- [ ] Vollbild-Modus
- [ ] Optional: Auswahl welche Sektionen sichtbar
#### 4.4 Berechtigungen
- [ ] Rolle "Rapport-Viewer" — sieht nur Rapport, nicht Karte / Journal
- [ ] Berechtigung pro Projekt vergebbar
---
### Phase 5 — Multi-User Live-Editing polish (optional, später)
Realtime-Sync läuft schon via Socket.IO — hier nur Verbesserungen.
- [ ] **Live-Cursor** anderer User auf Karte (mit Name)
- [ ] **Presence-Anzeige** ("Fabian schreibt im Journal", "Pepe zeichnet")
- [ ] **Soft-Locks** während Symbol bewegt wird
- [ ] **Conflict Resolution** bei gleichzeitigem Edit testen
---
## Priorisierungs-Matrix
| Phase | Aufwand | Impact | Priorität | Empfohlene Reihenfolge |
|-------|---------|--------|-----------|------------------------|
| 1 — Symbol-Architektur Redesign | L (3-4 W) | Sehr Hoch | ⭐⭐ Höchste | **1.** |
| 1.5 — Quick Polish | S | Mittel | 🔥 Hoch | parallel zu 1 |
| 3 — Linien-Typen | M | Hoch | ⭐ Hoch | **2.** |
| 4 — Rapport-Ansicht | L | Sehr Hoch (FU) | ⭐ Sehr Hoch | **3.** |
| 2 — Übungsmodus | L | Sehr Hoch | ⭐ Sehr Hoch | **4.** |
| 5 — Multi-User polish | M | Mittel | 📅 Mittel | später |
**Begründung der Reihenfolge**:
1. **Phase 1 zuerst** — Architektur-Umbau, weil alles andere darauf aufbaut. Sobald TenantSymbols + Kategorien sauber strukturiert sind, sind 1.3 (fehlende Symbole) und 1.4 (Stockwerke) trivial. Auch Phase 2 (Übungsmodus) profitiert: Übungs-Pakete als eigene Templates möglich.
2. **Phase 3 (Linien-Typen)** — kleine Erweiterung, hoher UX-Gewinn, unabhängig von Phase 1.
3. **Phase 4 (Rapport-Ansicht)** — USP gegenüber anderen Krokier-Tools, FU-Funktion fehlt überall sonst.
4. **Phase 2 (Übungsmodus)** — grosses Update; baut konzeptuell auf Phase 4 auf (Übungsauswertung ≈ Rapport).
5. **Phase 5** — Polish, kein User wartet darauf.
**Risiko-Hinweis Phase 1**:
- Schema-Migration von `Icon` (global) zu `SymbolTemplate` + Auto-Import muss **wasserdicht** sein, damit existierende Mandanten ihre Symbole nicht verlieren
- Vor Deploy: vollständiges DB-Backup (PostgreSQL) und MinIO-Backup (Icon-Files)
- Auf Staging testen falls möglich, sonst kleinen Test-Tenant zuerst migrieren
---
## Hinweise für Entwickler / KIs
- **Stack**: Next.js 15, React 19, MapLibre GL JS, Prisma, PostgreSQL, MinIO, Socket.IO, Zustand
- **Sprache**: Schweizerdeutsch / Hochdeutsch (Code englisch, UI deutsch)
- **Bei Symbol-Änderungen**: SVGs in `public/icons/` + Symbol-Definitionen + ggf. DB-Migration
- **Bei Schema-Änderungen**: Prisma Migration erstellen + DB-Backup vor Deploy
- **Vor Deploy**: Build testen (`npx next build`), commit + push, Portainer baut automatisch via Webhook
- **Realtime**: bei DB-Änderungen, die alle User sehen müssen, Socket-Event triggern
---
## Status-Tracking
> Status pro Aufgabe oben in Checkboxen. Hier kurzer Überblick:
- [-] **Phase 1** — Sprint A erledigt (Schema, Migration, Seed). Sprint BD offen.
- [ ] **Phase 2** — nicht gestartet
- [ ] **Phase 3** — nicht gestartet
- [ ] **Phase 4** — nicht gestartet
- [ ] **Phase 5** — nicht gestartet
---
**Letztes Update**: 2026-05-20
**Verantwortlich**: Pepe (adminpepe)

View File

@@ -51,7 +51,7 @@ const nextConfig = {
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https://*.tile.openstreetmap.org https://api.maptiler.com https://server.arcgisonline.com https://*.geo.admin.ch http://localhost:9000 http://minio:9000",
"font-src 'self' data:",
"connect-src 'self' ws: wss: https://api.maptiler.com https://*.tile.openstreetmap.org https://api.open-meteo.com https://server.arcgisonline.com https://*.geo.admin.ch",
"connect-src 'self' ws: wss: https://api.maptiler.com https://*.tile.openstreetmap.org https://nominatim.openstreetmap.org https://api.open-meteo.com https://server.arcgisonline.com https://*.geo.admin.ch",
"frame-ancestors 'self'",
"base-uri 'self'",
"form-action 'self'",

1503
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "lageplan",
"version": "1.1.0",
"version": "1.8.6",
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
"private": true,
"scripts": {
@@ -8,13 +8,21 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "vitest run",
"test:watch": "vitest",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:migrate": "prisma migrate dev",
"db:migrate:prod": "prisma migrate deploy",
"db:seed": "npx tsx prisma/seed.ts",
"db:seed": "node prisma/seed.js",
"db:studio": "prisma studio",
"postinstall": "prisma generate"
"postinstall": "prisma generate",
"db:seed:icons": "node prisma/seed-icons-only.js",
"db:seed:safe": "node prisma/seed.js",
"repair:features:dry-run": "node prisma/repair-features.js --dry-run",
"repair:features:apply": "node prisma/repair-features.js --apply",
"recover:symbols:dry-run": "node prisma/recover-symbols.js --dry-run",
"recover:symbols:apply": "node prisma/recover-symbols.js --apply"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
@@ -35,6 +43,8 @@
"@radix-ui/react-toast": "^1.2.0",
"@radix-ui/react-tooltip": "^1.1.0",
"@react-pdf/renderer": "^4.3.2",
"@simplewebauthn/browser": "^13.3.0",
"@simplewebauthn/server": "^13.3.2",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
@@ -46,6 +56,7 @@
"minio": "^7.1.3",
"next": "^15.1.0",
"nodemailer": "^6.9.8",
"otplib": "^12.0.1",
"qrcode": "^1.5.4",
"rdndmb-html5-to-touch": "^9.0.0",
"react": "^19.0.0",
@@ -61,7 +72,8 @@
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.1",
"zod": "^3.22.4"
"zod": "^3.22.4",
"zustand": "^5.0.11"
},
"prisma": {
"seed": "node prisma/seed.js"
@@ -81,7 +93,8 @@
"sharp": "^0.34.5",
"tailwindcss": "^3.4.1",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^4.1.10"
},
"browserslist": [
"last 2 Chrome versions",

View File

@@ -0,0 +1,267 @@
# Phase 1 — Symbol-Architektur Redesign: Detaillierter Plan
> Basierend auf: `docs/roadmap-feedback-fabian.md`
> Ziel: Mandantenspezifische Symbol-Bibliothek mit Template-Import, eigener Kategorisierung und vollständiger Entkopplung von globalen Icons.
> **Status: ABGESCHLOSSEN** ✅ (Sprints AD implementiert, getestet und gepusht)
---
## 1. Ziel & Konzept
**Problem heute:**
- `TenantSymbol` verweist auf `IconAsset` (global). Mandant kann zwar `customName` setzen, aber nicht die Kategorie ändern, das SVG bearbeiten oder Symbole aus verschiedenen Paketen frei mischen.
- Kategorien (`IconCategory`) sind global mit `tenantId` Override — uneinheitlich.
- Keine Möglichkeit, Vorlagen-Pakete (Feuerwehr CH, THW, Sanität) als Unit zu importieren.
**Lösung:**
- `SymbolTemplate` = globale, read-only Vorlagen (je Paket). Wird einmalig aus bestehenden `public/signaturen/` und `IconAsset` generiert.
- `TenantCategory` = pro Mandant, frei anlegbar/umbenennbar/sortierbar.
- `TenantSymbol` = pro Mandant, **vollständig eigenständig** (`name`, `svgPath`, `categoryId`). Kein Verweis mehr auf globale `IconAsset`.
- Mandant startet mit leerer Bibliothek und importiert Pakete nach Bedarf.
---
## 2. Datenmodell-Änderungen
### 2.1 Neue Modelle
```prisma
model SymbolTemplate {
id String @id @default(uuid())
packageId String // z.B. "feuerwehr-ch"
packageName String // z.B. "Feuerwehr Schweiz"
categoryName String // z.B. "Fahrzeuge"
name String
svgPath String // Relativer Pfad in public/ oder SVG-Inhalt
tags String[] @default([])
sortOrder Int @default(0)
@@index([packageId])
@@map("symbol_templates")
}
model TenantCategory {
id String @id @default(uuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
name String
description String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
symbols TenantSymbol[]
@@unique([tenantId, name])
@@index([tenantId])
@@map("tenant_categories")
}
```
### 2.2 Bestehendes `TenantSymbol` umbaut
**Vorher:**
```prisma
model TenantSymbol {
id String @id @default(uuid())
customName String?
sortOrder Int @default(0)
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
iconId String
icon IconAsset @relation(fields: [iconId], references: [id], onDelete: Cascade)
}
```
**Nachher:**
```prisma
model TenantSymbol {
id String @id @default(uuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
categoryId String?
category TenantCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
name String // endgültiger Anzeigename (kann aus Template importiert oder custom sein)
svgPath String? // z.B. "signaturen/TLF.svg" oder tenant-spezifischer MinIO-Key
sortOrder Int @default(0)
isUploaded Boolean @default(false) // true = eigener Upload, false = aus Template importiert
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Legacy-Feld für Migration (kann später entfernt werden)
migratedFromIconId String?
@@index([tenantId])
@@index([categoryId])
@@map("tenant_symbols")
}
```
> **Hinweis:** `IconAsset`, `IconCategory` und `hiddenIconIds` bleiben vorerst bestehen (read-only Legacy), werden aber nicht mehr für neue Features verwendet. In einer späteren Phase können sie entfernt werden.
---
## 3. Migration & Seed (Sprint A) ✅
- `prisma/migrations/20260520_symbol_architecture/migration.sql` — erstellt `symbol_templates`, `tenant_categories`, neue Spalten in `tenant_symbols`
- `prisma/seed-symbol-templates.ts` — füllt `SymbolTemplate` aus `public/signaturen/*.svg`
- `prisma/migrate-tenant-symbols.ts` — migriert bestehende `TenantSymbol`-Einträge
- `prisma/migrate.js` — erweitert um Migrationsschritte für Produktion
- Alle Seed-Skripte sind idempotent (`upsert` statt `deleteMany`)
---
## 4. API-Design (Sprint B) ✅
### 4.1 Templates
```
GET /api/templates
→ { packages: [{ packageId, packageName, categoryCount, symbolCount, previewSymbols }] }
POST /api/templates/import
Body: { packageId }
→ importiert alle Symbole des Pakets als TenantSymbols
→ antwortet mit { imported, skipped, message }
```
**Implementiert:**
- `src/app/api/templates/route.ts` — GET mit `groupBy` Aggregation und Vorschau
- `src/app/api/templates/import/route.ts` — POST mit Auto-Kategorie-Erstellung und Deduplikation
### 4.2 Tenant Categories
```
GET /api/tenant/categories → [{ id, name, sortOrder, description }]
POST /api/tenant/categories Body: { name } → neue Kategorie
PATCH /api/tenant/categories Body: { id, name } → umbenennen
DELETE /api/tenant/categories?id=... → 204 oder 409 wenn nicht leer
```
**Implementiert:** `src/app/api/tenant/categories/route.ts`
### 4.3 Tenant Symbols
```
GET /api/tenant/symbols?grouped=true|false
→ grouped=true: { groups: [{ category, symbols }], symbols: [...] }
→ grouped=false: { symbols: [...] }
POST /api/tenant/symbols
- Multipart: { file, categoryId? } → Upload nach MinIO
- JSON: { templateId, categoryId? } → aus SymbolTemplate
- JSON: { iconId, customName?, categoryId? } → aus IconAsset (Legacy)
PATCH /api/tenant/symbols Body: { id, name?, customName?, categoryId?, sortOrder? }
DELETE /api/tenant/symbols Body: { id } → löscht TenantSymbol
```
**Implementiert:** `src/app/api/tenant/symbols/route.ts` + `src/app/api/tenant/symbols/[id]/image/route.ts`
### 4.4 Icon-Serving (TenantSymbol-First Lookup)
```
GET /api/icons/:id/image
→ 1. TenantSymbol (svgPath aus public/ oder MinIO)
→ 2. Fallback: IconAsset (legacy)
GET /api/tenant/symbols/:id/image
→ TenantSymbol-Bild aus MinIO oder public/
```
**Implementiert:**
- `src/app/api/icons/[id]/image/route.ts`
- `src/app/api/tenant/symbols/[id]/image/route.ts`
---
## 5. Frontend-Änderungen (Sprints C + D) ✅
### 5.1 Admin — Symbol-Manager (`src/components/admin/symbol-manager.tsx`)
**Tabs:**
1. **Meine Symbole** — nach TenantCategory gruppierte Symbol-Liste
- Expandable Kategorien
- SymbolCard: Bild, Name (inline-edit), Kategorie-Select, Löschen
- Suche über alle Symbole
2. **Kategorien** — CRUD für TenantCategory
- Neue Kategorie erstellen
- Umbenennen (inline)
- Löschen nur wenn leer
3. **Vorlagen importieren** — Template-Pakete als Cards
- Vorschau der ersten 4 Symbole
- 1-Klick Import
**Actions:**
- Upload-Button → Dialog mit Drag & Drop + Kategorie-Auswahl
- Import-Button → Dialog mit allen verfügbaren Paketen
### 5.2 Sidebar — RightSidebar (`src/components/layout/right-sidebar.tsx`)
- Verwendet neue `tenantSymbolGroups` aus `/api/icons`
- **Meine Symbole** Sektion: nach TenantCategory gruppiert, expandable
- **Bibliothek** Sektion: globale IconAsset-Kategorien (Legacy, read-only)
- Drag & Drop funktioniert mit neuen TenantSymbol-IDs
### 5.3 API /icons — Kompatibilität
`src/app/api/icons/route.ts` liefert:
- `categories` — Legacy IconAsset-Kategorien (für alte Clients)
- `mySymbols` — Legacy TenantSymbol-Liste (für alte Clients)
- `tenantSymbols` — Neue flache TenantSymbol-Liste
- `tenantSymbolGroups` — Neue gruppierte Liste für Sidebar
---
## 6. Dateien: Create / Modify
### Neue Dateien ✅
| Pfad | Beschreibung |
|------|-------------|
| `prisma/migrations/20260520_symbol_architecture/migration.sql` | Migration neue Tabellen + Spalten |
| `prisma/seed-symbol-templates.ts` | Seed: `public/signaturen/*.svg``SymbolTemplate` |
| `src/app/api/templates/route.ts` | GET /api/templates |
| `src/app/api/templates/import/route.ts` | POST /api/templates/import |
| `src/app/api/tenant/categories/route.ts` | CRUD TenantCategory |
| `src/app/api/tenant/symbols/[id]/image/route.ts` | TenantSymbol Bild-Serving |
### Modifizierte Dateien ✅
| Pfad | Änderung |
|------|----------|
| `prisma/schema.prisma` | Neue Modelle + TenantSymbol Refactor |
| `src/app/api/tenant/symbols/route.ts` | Refactor: Gruppierung, Upload, CRUD |
| `src/app/api/icons/route.ts` | Liefert tenantSymbolGroups |
| `src/app/api/icons/[id]/image/route.ts` | TenantSymbol-First Lookup |
| `src/components/admin/symbol-manager.tsx` | Vollständiger Umbau mit Kategorien, Import, Upload |
| `src/components/layout/right-sidebar.tsx` | Gruppierte TenantSymbol-Anzeige |
---
## 7. Ausführungsreihenfolge (Tatsächlich)
| Sprint | Status | Inhalt |
|--------|--------|--------|
| **A** | ✅ Fertig | Schema, Migration, Seed, Daten-Migration |
| **B** | ✅ Fertig | Templates API, Categories API, Symbols API erweitert |
| **C** | ✅ Fertig | Admin UI: Symbol-Manager mit Kategorien, Import, Upload |
| **D** | ✅ Fertig | Frontend Sidebar gruppiert nach TenantCategory |
---
## 8. Risiken & Entscheidungen (Umgesetzt)
| Thema | Entscheidung |
|-------|-------------|
| **SVG-Speicherort** | Templates: `public/signaturen/` Pfade in DB (read-only, kein MinIO-Overhead). Uploads: MinIO `tenant-{id}/symbols/`. |
| **Migration alter Tenants** | Vollautomatisch via `prisma/migrate.js` + `prisma/migrate-tenant-symbols.ts`. |
| **Sidebar vs SymbolPalette** | `RightSidebar` direkt angepasst, keine separate `SymbolPalette`-Komponente nötig. |
| **instanceLabel / Stockwerke** | Nicht in Sprint D enthalten (laut Roadmap Phase 1.4 — kann separat geplant werden). |
---
*Plan erstellt: 2026-05-20*
*Phase 1 abgeschlossen: 2026-05-20*

View File

@@ -0,0 +1,112 @@
/**
* Post-Migration Script: Migrate existing TenantSymbols to new architecture.
*
* Run AFTER applying the schema migration (20260520_symbol_architecture).
*
* Steps:
* 1. Create a default "Meine Symbole" TenantCategory for each tenant that has TenantSymbols
* 2. Migrate existing TenantSymbols: set name, svgPath, categoryId, migratedFromIconId
* 3. Verify consistency
*
* Usage:
* npx ts-node prisma/migrate-tenant-symbols.ts
*/
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
console.log('🔧 Migrating existing TenantSymbols...')
// Step 1: Get all tenants that have existing tenantSymbols
const tenantsWithSymbols = await (prisma as any).tenantSymbol.groupBy({
by: ['tenantId'],
_count: { id: true },
})
console.log(`Found ${tenantsWithSymbols.length} tenants with existing symbols`)
let categoriesCreated = 0
let symbolsMigrated = 0
for (const group of tenantsWithSymbols) {
const tenantId = group.tenantId
// Step 2: Create default category for this tenant
let defaultCategory = await (prisma as any).tenantCategory.findFirst({
where: { tenantId, name: 'Meine Symbole' },
})
if (!defaultCategory) {
defaultCategory = await (prisma as any).tenantCategory.create({
data: {
tenantId,
name: 'Meine Symbole',
sortOrder: 0,
},
})
categoriesCreated++
}
// Step 3: Migrate all tenantSymbols for this tenant
const tenantSymbols = await (prisma as any).tenantSymbol.findMany({
where: { tenantId },
include: { icon: true },
})
for (const ts of tenantSymbols) {
const updates: any = {}
// name = customName || icon.name
if (!ts.name) {
updates.name = ts.customName || ts.icon?.name || 'Unbenannt'
}
// svgPath = icon.fileKey
if (!ts.svgPath && ts.icon?.fileKey) {
updates.svgPath = ts.icon.fileKey
}
// categoryId = default category
if (!ts.categoryId) {
updates.categoryId = defaultCategory.id
}
// migratedFromIconId = current iconId
if (!ts.migratedFromIconId) {
updates.migratedFromIconId = ts.iconId
}
if (Object.keys(updates).length > 0) {
await (prisma as any).tenantSymbol.update({
where: { id: ts.id },
data: updates,
})
symbolsMigrated++
}
}
}
console.log(`✅ Done: ${categoriesCreated} categories created, ${symbolsMigrated} symbols migrated`)
// Verification
const unmappedCount = await (prisma as any).tenantSymbol.count({
where: { categoryId: null },
})
if (unmappedCount > 0) {
console.warn(`⚠️ ${unmappedCount} tenantSymbols still have no categoryId!`)
} else {
console.log('✅ All tenantSymbols have a category assigned')
}
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})

View File

@@ -2,6 +2,12 @@
* Database migration script using PrismaClient raw SQL.
* Does NOT require the Prisma CLI (npx prisma) — only the runtime client.
* Safe to run multiple times (all statements are idempotent).
*
* SAFETY RULES:
* - NO deleteMany / DELETE / TRUNCATE on icon_assets, icon_categories,
* tenant_symbols, or features. These contain user data.
* - All operations must be idempotent (safe to re-run).
* - In production, destructive operations are blocked.
*/
const { PrismaClient } = require('@prisma/client')
@@ -53,6 +59,7 @@ async function migrate() {
// Users
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "lastLoginAt" TIMESTAMP`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "emailVerified" BOOLEAN DEFAULT true`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "isActive" BOOLEAN NOT NULL DEFAULT true`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "emailVerificationToken" TEXT`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "resetToken" TEXT`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "resetTokenExpiry" TIMESTAMP`,
@@ -64,6 +71,9 @@ async function migrate() {
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "planBounds" JSONB`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "einsatzleiter" TEXT`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "journalfuehrer" TEXT`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "mode" TEXT NOT NULL DEFAULT 'EINSATZ'`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "exerciseEvaluation" TEXT`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "featuresVersion" INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingById" TEXT`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingUserName" TEXT`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingSessionId" TEXT`,
@@ -72,6 +82,15 @@ async function migrate() {
// Journal
`ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS "isCorrected" BOOLEAN DEFAULT false`,
`ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS "correctionOfId" TEXT`,
// Modul-Baukasten (Cockpit-Konfiguration pro Mandant)
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "modulesConfig" JSONB`,
// Windrichtung fürs Lagebild
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "windDirection" INTEGER`,
// Öffentlicher Nur-Ansicht-Teilen-Link (live, optional PIN)
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "shareToken" TEXT`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "sharePin" TEXT`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "shareEnabled" BOOLEAN NOT NULL DEFAULT false`,
`CREATE UNIQUE INDEX IF NOT EXISTS "projects_shareToken_key" ON projects ("shareToken")`,
]
let added = 0
for (const sql of columnMigrations) {
@@ -110,21 +129,122 @@ async function migrate() {
"createdById" TEXT REFERENCES users(id) ON DELETE SET NULL,
UNIQUE("tenantId", "reportNumber")
)`,
// Exercise goals (Übungs-Cockpit)
`CREATE TABLE IF NOT EXISTS exercise_goals (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
text TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'OPEN',
note TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"projectId" TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE
)`,
// Modul-Baukasten: generische Zeilen für Tabellen-Module
`CREATE TABLE IF NOT EXISTS module_items (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
"moduleId" TEXT NOT NULL,
data JSONB NOT NULL DEFAULT '{}',
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"projectId" TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS module_items_project_module_idx ON module_items("projectId", "moduleId")`,
// Rechtsdokumente (versioniert, bewusst gesetzte Version)
`CREATE TABLE IF NOT EXISTS legal_documents (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
type TEXT NOT NULL,
version TEXT NOT NULL,
title TEXT NOT NULL,
"publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"effectiveAt" TIMESTAMP(3),
"contentHash" TEXT NOT NULL,
url TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(type, version)
)`,
`CREATE INDEX IF NOT EXISTS legal_documents_type_active_idx ON legal_documents(type, "isActive")`,
// Revisionssichere Zustimmungen — nie überschreiben, neue Version = neuer Datensatz
`CREATE TABLE IF NOT EXISTS legal_acceptances (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
"userId" TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
"organizationId" TEXT,
"documentType" TEXT NOT NULL,
"documentVersion" TEXT NOT NULL,
"contentHash" TEXT NOT NULL,
"acceptedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
locale TEXT,
context TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE("userId", "organizationId", "documentType", "documentVersion", context)
)`,
`CREATE INDEX IF NOT EXISTS legal_acceptances_user_idx ON legal_acceptances("userId")`,
`CREATE INDEX IF NOT EXISTS legal_acceptances_doc_idx ON legal_acceptances("documentType", "documentVersion")`,
`ALTER TABLE legal_acceptances ADD COLUMN IF NOT EXISTS "ipAddress" TEXT`,
`ALTER TABLE legal_acceptances ADD COLUMN IF NOT EXISTS "userAgent" TEXT`,
// MFA / Zwei-Faktor
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "totpSecret" TEXT`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "totpConfirmedAt" TIMESTAMP(3)`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "mfaBackupCodes" TEXT[] NOT NULL DEFAULT '{}'`,
`CREATE TABLE IF NOT EXISTS webauthn_credentials (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
"userId" TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
"credentialId" TEXT NOT NULL UNIQUE,
"publicKey" TEXT NOT NULL,
counter INTEGER NOT NULL DEFAULT 0,
transports TEXT,
"deviceName" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastUsedAt" TIMESTAMP(3)
)`,
`CREATE INDEX IF NOT EXISTS webauthn_credentials_user_idx ON webauthn_credentials("userId")`,
]
for (const sql of tableMigrations) {
try { await prisma.$executeRawUnsafe(sql) } catch (e) { /* table might already exist */ }
}
// ─── Step 4b: Rechtsdokumente seeden/aktivieren (idempotent) ───
// WICHTIG: Diese Metadaten müssen mit src/config/legal.ts übereinstimmen. Version bewusst
// erhöhen, wenn sich der Inhalt WESENTLICH ändert (löst Re-Consent aus).
console.log(' [4b] Seeding legal documents...')
const crypto = require('crypto')
const LEGAL_DOCS = [
{ type: 'TERMS', version: '1.0.0', title: 'Nutzungsbedingungen', url: '/nutzungsbedingungen', publishedAt: '2026-07-23' },
{ type: 'PRIVACY', version: '1.0.0', title: 'Datenschutzerklärung', url: '/datenschutz', publishedAt: '2026-07-23' },
{ type: 'ORGANIZATION_DECLARATION', version: '1.0.0', title: 'Organisationsbestätigung', url: '/verantwortungsvolle-nutzung', publishedAt: '2026-07-23' },
{ type: 'RESPONSIBLE_USE', version: '1.0.0', title: 'Verantwortungsvolle Nutzung', url: '/verantwortungsvolle-nutzung', publishedAt: '2026-07-23' },
{ type: 'DATA_PROCESSING_AGREEMENT', version: '1.0.0', title: 'Datenschutzvereinbarung für Organisationen', url: '/organisationen/datenschutzvereinbarung', publishedAt: '2026-07-23' },
]
for (const doc of LEGAL_DOCS) {
const contentHash = crypto.createHash('sha256').update(`${doc.type}@${doc.version}`).digest('hex')
try {
// Neue Version anlegen (falls noch nicht vorhanden), als aktiv markieren
await prisma.$executeRawUnsafe(
`INSERT INTO legal_documents (type, version, title, url, "contentHash", "publishedAt", "effectiveAt", "isActive")
VALUES ($1,$2,$3,$4,$5,$6,$6,true)
ON CONFLICT (type, version) DO UPDATE SET title = EXCLUDED.title, url = EXCLUDED.url, "isActive" = true`,
doc.type, doc.version, doc.title, doc.url, contentHash, new Date(doc.publishedAt)
)
// Andere Versionen desselben Typs deaktivieren (nur eine aktive je Typ)
await prisma.$executeRawUnsafe(
`UPDATE legal_documents SET "isActive" = false WHERE type = $1 AND version <> $2`,
doc.type, doc.version
)
} catch (e) { /* table might not exist yet on very first run */ }
}
// ─── Step 5: Set safe defaults ───
console.log(' [5/7] Setting defaults...')
try {
await prisma.$executeRawUnsafe(`UPDATE tenants SET "subscriptionStatus" = 'ACTIVE' WHERE "subscriptionStatus" = 'TRIAL'`)
} catch (e) { /* ignore */ }
// ─── Step 6: Clean up orphan users ───
console.log(' [6/7] Cleaning up orphan users...')
// ─── Step 6: Detect orphan users (log only, no deletion) ───
console.log(' [6/7] Checking for orphan users...')
try {
// Find users who are NOT SERVER_ADMIN and have NO tenant membership
const orphans = await prisma.user.findMany({
where: {
role: { not: 'SERVER_ADMIN' },
@@ -133,22 +253,15 @@ async function migrate() {
select: { id: true, email: true, name: true },
})
if (orphans.length > 0) {
console.log(` Found ${orphans.length} orphan user(s):`)
console.log(` ⚠️ Found ${orphans.length} orphan user(s) (NOT deleting — manual review required):`)
for (const o of orphans) {
console.log(` - ${o.email} (${o.name})`)
}
// Delete orphan users and their related data
await prisma.user.deleteMany({
where: {
id: { in: orphans.map(o => o.id) },
},
})
console.log(` 🗑️ ${orphans.length} orphan user(s) removed`)
} else {
console.log(' No orphan users found')
}
} catch (e) {
console.log(' Orphan cleanup skipped:', e.message)
console.log(' Orphan check skipped:', e.message)
}
// ─── Step 7: Backfill logoFileKey from logoUrl ───
@@ -214,6 +327,210 @@ async function migrate() {
console.log(' Privacy consent columns skipped:', e.message)
}
// ─── Step 12: Create tenant_symbols table (CRITICAL — fail-fast) ───
console.log(' [12] Creating tenant_symbols table...')
try {
await prisma.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS tenant_symbols (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
"isActive" BOOLEAN NOT NULL DEFAULT true,
"tenantId" TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
"iconId" TEXT REFERENCES icon_assets(id) ON DELETE SET NULL,
UNIQUE("tenantId", "iconId")
)
`)
console.log(' tenant_symbols table created (or already exists)')
} catch (e) {
console.error(' ❌ CRITICAL: tenant_symbols table creation failed:', e.message)
throw e
}
// ─── Step 13: Create symbol_templates table ───
console.log(' [13] Creating symbol_templates table...')
try {
await prisma.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS symbol_templates (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
"fileKey" TEXT NOT NULL,
"originalFilename" TEXT NOT NULL,
"displayName" TEXT,
"categoryName" TEXT,
"svgPath" TEXT,
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE("fileKey")
)
`)
console.log(' symbol_templates table created (or already exists)')
} catch (e) {
console.log(' symbol_templates table skipped:', e.message)
}
// ─── Step 14: Create tenant_categories table (CRITICAL — fail-fast) ───
console.log(' [14] Creating tenant_categories table...')
try {
await prisma.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS tenant_categories (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL,
"description" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"tenantId" TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE
)
`)
console.log(' tenant_categories table created (or already exists)')
} catch (e) {
console.error(' ❌ CRITICAL: tenant_categories table creation failed:', e.message)
throw e
}
// ─── Step 14b: Ensure tenant_categories has NOT NULL defaults ───
console.log(' [14b] Ensuring tenant_categories columns...')
const tcColumns = [
`ALTER TABLE tenant_categories ALTER COLUMN "isActive" SET DEFAULT true`,
`ALTER TABLE tenant_categories ALTER COLUMN "sortOrder" SET DEFAULT 0`,
`ALTER TABLE tenant_categories ALTER COLUMN "createdAt" SET DEFAULT CURRENT_TIMESTAMP`,
`ALTER TABLE tenant_categories ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP`,
]
for (const sql of tcColumns) {
try { await prisma.$executeRawUnsafe(sql) } catch (e) { /* ignore */ }
}
// ─── Step 15: Extend tenant_symbols with Phase 1 columns ───
console.log(' [15] Extending tenant_symbols with Phase 1 columns...')
const tenantSymbolColumns = [
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "name" TEXT`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "svgPath" TEXT`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "isUploaded" BOOLEAN NOT NULL DEFAULT false`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "categoryId" TEXT`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "migratedFromIconId" TEXT`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "customName" TEXT`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "sortOrder" INTEGER`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP`,
]
let tsAdded = 0
for (const sql of tenantSymbolColumns) {
try { await prisma.$executeRawUnsafe(sql); tsAdded++ } catch (e) { /* ignore */ }
}
console.log(` ${tsAdded}/${tenantSymbolColumns.length} tenant_symbol columns added`)
// ─── Step 15b: Add tenant_symbols.categoryId FK separately (idempotent) ───
console.log(' [15b] Adding tenant_symbols.categoryId FK...')
try {
await prisma.$executeRawUnsafe(`
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'tenant_symbols_categoryId_fkey'
AND table_name = 'tenant_symbols'
) THEN
ALTER TABLE tenant_symbols
ADD CONSTRAINT "tenant_symbols_categoryId_fkey"
FOREIGN KEY ("categoryId") REFERENCES tenant_categories(id) ON DELETE SET NULL;
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'categoryId FK skipped: %', SQLERRM;
END $$;
`)
console.log(' ✅ tenant_symbols.categoryId FK added')
} catch (e) {
console.log(' categoryId FK skipped:', e.message)
}
// ─── Step 16: Fix tenant_symbols FK (CASCADE → SET NULL) ───
console.log(' [16] Fixing tenant_symbols.iconId FK (CASCADE → SET NULL)...')
try {
// Make iconId nullable
await prisma.$executeRawUnsafe(`ALTER TABLE tenant_symbols ALTER COLUMN "iconId" DROP NOT NULL`)
// Drop old cascade FK and recreate with SET NULL
await prisma.$executeRawUnsafe(`
DO $$ BEGIN
-- Drop existing FK constraint (name varies)
ALTER TABLE tenant_symbols DROP CONSTRAINT IF EXISTS "tenant_symbols_iconId_fkey";
ALTER TABLE tenant_symbols DROP CONSTRAINT IF EXISTS "tenant_symbols_iconId_icon_assets_id_fk";
-- Recreate with SET NULL
ALTER TABLE tenant_symbols
ADD CONSTRAINT "tenant_symbols_iconId_fkey"
FOREIGN KEY ("iconId") REFERENCES icon_assets(id) ON DELETE SET NULL;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'FK fix skipped: %', SQLERRM;
END $$;
`)
console.log(' ✅ tenant_symbols.iconId FK is now ON DELETE SET NULL')
} catch (e) {
console.log(' FK fix skipped:', e.message)
}
// ─── Step 17: Drop unique constraint on tenant_symbols(tenantId, iconId) ───
console.log(' [17] Dropping UNIQUE(tenantId, iconId) on tenant_symbols...')
try {
await prisma.$executeRawUnsafe(`ALTER TABLE tenant_symbols DROP CONSTRAINT IF EXISTS "tenant_symbols_tenantId_iconId_key"`)
console.log(' ✅ Unique constraint dropped (duplicates now allowed)')
} catch (e) {
console.log(' Unique constraint drop skipped:', e.message)
}
// ─── Step 18: Migrate legacy tenantSymbols (name, svgPath, categoryId, migratedFromIconId) ───
console.log(' [18] Migrating legacy tenantSymbols data...')
try {
const tenantsWithSymbols = await prisma.$queryRawUnsafe(`
SELECT "tenantId", COUNT(id) as cnt FROM tenant_symbols GROUP BY "tenantId"
`)
let catsCreated = 0
let symsMigrated = 0
for (const row of tenantsWithSymbols) {
const tenantId = row.tenantId
// Create default category if none exists for this tenant
let defaultCat = await prisma.$queryRawUnsafe(`
SELECT id FROM tenant_categories WHERE "tenantId" = '${tenantId}' AND name = 'Meine Symbole' LIMIT 1
`)
if (!defaultCat || !defaultCat.length) {
const newCatId = crypto.randomUUID()
await prisma.$executeRawUnsafe(`
INSERT INTO tenant_categories (id, "tenantId", name, "sortOrder", "createdAt", "updatedAt")
VALUES ('${newCatId}', '${tenantId}', 'Meine Symbole', 0, NOW(), NOW())
`)
defaultCat = [{ id: newCatId }]
catsCreated++
}
const catId = defaultCat[0].id
// Migrate symbols: set name, svgPath, categoryId, migratedFromIconId where null
await prisma.$executeRawUnsafe(`
UPDATE tenant_symbols ts
SET
name = COALESCE(ts.name, ts."customName", ia.name, 'Unbenannt'),
"svgPath" = COALESCE(ts."svgPath", ia."fileKey"),
"categoryId" = COALESCE(ts."categoryId", '${catId}'),
"migratedFromIconId" = COALESCE(ts."migratedFromIconId", ts."iconId"),
"updatedAt" = NOW()
FROM icon_assets ia
WHERE ts."tenantId" = '${tenantId}'
AND ia.id = ts."iconId"
AND (ts.name IS NULL OR ts."svgPath" IS NULL OR ts."categoryId" IS NULL OR ts."migratedFromIconId" IS NULL)
`)
// Also handle symbols where iconId is already null (orphaned) — at least set category & name
await prisma.$executeRawUnsafe(`
UPDATE tenant_symbols
SET
name = COALESCE(name, "customName", 'Unbenannt'),
"categoryId" = COALESCE("categoryId", '${catId}'),
"updatedAt" = NOW()
WHERE "tenantId" = '${tenantId}'
AND (name IS NULL OR "categoryId" IS NULL)
`)
symsMigrated++
}
console.log(`${catsCreated} default categories created, ${symsMigrated} tenants processed`)
} catch (e) {
console.log(' Legacy migration skipped:', e.message)
}
console.log('✅ Database migrations complete')
}

151
prisma/recover-features.js Normal file
View File

@@ -0,0 +1,151 @@
/**
* Recovery-Skript für Zeichnungen (Features) nach Symbol-Verlust
*
* Problem: Features verweisen auf gelöschte iconId-UUIDs.
* Die imageUrl ist "/api/icons/{alte-uuid}/image" → 404.
*
* Lösung:
* Die alte UUID→SVG-Zuordnung ist verloren. ABER:
* - Alle bestehenden IconAssets wurden per Upsert neu erstellt (gleiche fileKeys)
* - Jedes Feature hat noch die alte iconId in properties
* - Die Sidebar liefert jetzt NEUE iconIds
*
* Dieses Skript:
* 1. Listet alle broken Symbol-Features (iconId zeigt auf gelöschtes Icon)
* 2. Für Features wo imageUrl auf /signaturen/ zeigt: direkt fixbar
* 3. Für Features mit /api/icons/{uuid}/image: UUID ist verloren → Renderer zeigt ⚠️
* 4. Zählt und listet betroffene Projekte
*
* Der Renderer wurde angepasst (map-view.tsx): broken Symbole zeigen jetzt
* ein ⚠️-Platzhalter statt nichts. User können das Symbol manuell ersetzen.
*
* Ausführung:
* DATABASE_URL=... node prisma/recover-features.js [--dry-run]
*/
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
const DRY_RUN = process.argv.includes('--dry-run')
async function main() {
console.log('🔧 FEATURE RECOVERY — Analyse & Reparatur')
console.log(` Mode: ${DRY_RUN ? '🔍 DRY RUN' : '⚡ LIVE'}`)
console.log('')
// ─── 1. Lade alle aktuellen IconAssets ───
const allIcons = await prisma.iconAsset.findMany({
select: { id: true, name: true, fileKey: true },
})
console.log(`📦 ${allIcons.length} IconAssets in der DB`)
const iconIdSet = new Set(allIcons.map(i => i.id))
const fileKeyToIcon = {}
for (const icon of allIcons) {
fileKeyToIcon[icon.fileKey] = icon
}
// ─── 2. Lade alle Symbol-Features ───
const features = await prisma.feature.findMany({
where: { type: 'symbol' },
include: { project: { select: { id: true, name: true, tenantId: true } } },
})
console.log(`🖼️ ${features.length} Symbol-Features gefunden`)
console.log('')
let ok = 0
let fixedViaFileKey = 0
let broken = 0
const brokenByProject = {}
for (const feature of features) {
const props = feature.properties || {}
const iconId = props.iconId || ''
const imageUrl = props.imageUrl || ''
// ─── Check 1: iconId still exists? ───
if (iconId && iconIdSet.has(iconId)) {
ok++
continue
}
// ─── Check 2: imageUrl points to static /signaturen/ file? ───
const sigMatch = imageUrl.match(/\/signaturen\/(.+\.svg)/)
if (sigMatch) {
const fileKey = `signaturen/${sigMatch[1]}`
const matchingIcon = fileKeyToIcon[fileKey]
if (matchingIcon) {
// Fix: Update iconId to new valid ID
const newProps = {
...props,
iconId: matchingIcon.id,
imageUrl: `/api/icons/${matchingIcon.id}/image`,
_recovered: true,
_oldIconId: iconId,
}
if (!DRY_RUN) {
await prisma.feature.update({
where: { id: feature.id },
data: { properties: newProps },
})
}
fixedViaFileKey++
console.log(`${feature.id}${matchingIcon.name} (via fileKey)`)
continue
}
}
// ─── Check 3: imageUrl is /api/icons/{uuid}/image? ───
// The uuid in the URL = old iconId = deleted → can't resolve
// Mark as broken
broken++
const projName = feature.project?.name || feature.projectId
if (!brokenByProject[projName]) brokenByProject[projName] = []
brokenByProject[projName].push({
featureId: feature.id,
iconId,
imageUrl,
})
}
console.log('')
console.log('═══════════════════════════════════════════')
console.log(` ✅ OK (iconId existiert): ${ok}`)
console.log(` 🔧 Gefixt (via fileKey): ${fixedViaFileKey}`)
console.log(` ❌ Broken (UUID verloren): ${broken}`)
console.log(` 📊 Total: ${features.length}`)
console.log('═══════════════════════════════════════════')
if (broken > 0) {
console.log('')
console.log('❌ BROKEN FEATURES pro Projekt:')
for (const [projName, items] of Object.entries(brokenByProject)) {
console.log(` 📄 "${projName}": ${items.length} Symbole`)
for (const item of items) {
console.log(` - Feature ${item.featureId} (iconId: ${item.iconId.substring(0, 8)}...)`)
}
}
console.log('')
console.log('💡 Diese Symbole zeigen jetzt ein ⚠️ auf der Karte.')
console.log(' User müssen das Symbol manuell löschen und neu platzieren.')
console.log(' (Select-Mode → Symbol anklicken → DEL → neues Symbol aus Sidebar ziehen)')
}
if (broken === 0 && fixedViaFileKey === 0) {
console.log('')
console.log('🎉 Alle Symbol-Features sind OK! Keine Reparatur nötig.')
}
if (DRY_RUN) {
console.log('')
console.log('🔍 DRY RUN — Keine Änderungen geschrieben.')
}
}
main()
.then(async () => { await prisma.$disconnect() })
.catch(async (e) => {
console.error('❌ Fehler:', e)
await prisma.$disconnect()
process.exit(1)
})

258
prisma/recover-symbols.js Normal file
View File

@@ -0,0 +1,258 @@
/**
* Recovery-Skript für gelöschte/neu erstellte Symbole
*
* Stellt sicher, dass alle Icons aus public/signaturen/ als IconAssets
* in der DB vorhanden sind und für alle Tenants aktiviert sind.
*
* Unterstützt Dry-Run und Apply-Modus.
*
* Ausführung:
* Analyse: node prisma/recover-symbols.js --dry-run
* Anwenden: node prisma/recover-symbols.js --apply
*/
const { PrismaClient } = require('@prisma/client')
const fs = require('fs')
const path = require('path')
const prisma = new PrismaClient()
const IS_DRY_RUN = process.argv.includes('--dry-run')
const IS_APPLY = process.argv.includes('--apply')
const IS_PROD = process.env.NODE_ENV === 'production'
// ─── Safety Guards ─────────────────────────────────────────────────────────
if (!IS_DRY_RUN && !IS_APPLY) {
console.error('❌ Fehler: Bitte --dry-run oder --apply angeben')
console.error('')
console.error(' Analyse: node prisma/recover-symbols.js --dry-run')
console.error(' Anwenden: node prisma/recover-symbols.js --apply')
process.exit(1)
}
if (IS_DRY_RUN && IS_APPLY) {
console.error('❌ Fehler: --dry-run und --apply können nicht zusammen verwendet werden')
process.exit(1)
}
if (IS_APPLY && IS_PROD) {
console.log('⚠️ Production-Umgebung erkannt. Fortfahren in 3 Sekunden...')
await new Promise(r => setTimeout(r, 3000))
}
console.log(IS_DRY_RUN ? '🔍 DRY-RUN MODUS' : '🔧 APPLY-MODUS')
// ─── Kategorie-Definitionen ────────────────────────────────────────────────
const catDefs = [
{ name: 'Feuer / Brand', description: 'Feuer, Brand, Brandschutz', sortOrder: 1,
patterns: ['Feuer', 'Brand', 'Explosion', 'Entrauchung', 'Rauch', 'Kamin', 'Luefter', 'Sprinkler'] },
{ name: 'Wasser', description: 'Wasser, Hydranten, Leitungen', sortOrder: 2,
patterns: ['Wasser', 'Hydrant', 'Reservoir', 'Druckleitung', 'Druckleistung', 'Transportleitung',
'Schieber', 'Wasserloeschposten', 'Wasserversorgung', 'Wasserleitung', 'Ueberschwemmung',
'Offener_Wasserverlauf', 'Moeglicher_Wasserbezugsort', 'Stehendes_Gewaesser',
'Kleinloeschgeraet', 'Gefahr_durch_Loeschen_mit_Wasser'] },
{ name: 'Gefahren / Stoffe', description: 'Gefahrstoffe, Chemie, ABC', sortOrder: 3,
patterns: ['Gefaehrlich', 'Chemie', 'Chemikalien', 'Biologisch', 'Radioaktiv', 'Gas', 'Elektri',
'Elektrotableau', 'Achtung', 'Leitungsdaehte'] },
{ name: 'Rettung / Personen', description: 'Rettung, Sanität, Personen', sortOrder: 4,
patterns: ['Rettung', 'Retten', 'Sanitaet', 'Patienten', 'Sammelplatz', 'Sammelstelle',
'Totensammelstelle', 'Sprungretter', 'Helikopterlandeplatz'] },
{ name: 'Leitern / Geräte', description: 'Leitern, Fahrzeuge, Geräte', sortOrder: 5,
patterns: ['Leiter', 'Anhaengeleiter', 'Strebenleiter', 'ADL', 'TLF', 'HRF', 'SWHP'] },
{ name: 'Gebäude / Schäden', description: 'Gebäude, Zerstörung, Schäden', sortOrder: 6,
patterns: ['Beschaedigung', 'Teilzerstoerung', 'Totalzerstoerung', 'Decke_eingestuerzt',
'Umfassungswaende', 'AnzahlGeschosse', 'Eingang', 'Treppen', 'Lift', 'Bruecke',
'Rutschgebiet', 'Strasse', 'Bahnlin'] },
{ name: 'Einsatzführung', description: 'Führung, Organisation, Kommunikation', sortOrder: 7,
patterns: ['Einsatzleiter', 'KP_Font', 'Offizier', 'Beobachtungsposten', 'Kontrollstelle',
'Funk', 'Informationszentrum', 'Medienkontaktstelle', 'Fernsignaltableau',
'Materialdepot', 'Schluesseldepot', 'Abschnitt', 'Absperrung'] },
{ name: 'Organisationen', description: 'Feuerwehr, Polizei, Armee, Zivilschutz', sortOrder: 8,
patterns: ['Feuerwehr', 'Polizei', 'Armee', 'Zivilschutz', 'Chemiewehr', 'MS_de', 'Anmarsch'] },
{ name: 'Entwicklung / Taktik', description: 'Entwicklungsgrenzen, Ausbreitung', sortOrder: 9,
patterns: ['Entwicklungsgrenze', 'Horizontale_Entwicklung', 'Vertikale_Entwicklung',
'VertikaleEntwicklung', 'Unfall'] },
{ name: 'Verschiedenes', description: 'Weitere Signaturen', sortOrder: 10,
patterns: ['Massstab', 'Nordrichtung', 'Windrichtung'] },
]
function findCategory(filename) {
for (const def of catDefs) {
for (const pattern of def.patterns) {
if (filename.includes(pattern)) return def.name
}
}
return 'Verschiedenes'
}
// ─── Main ──────────────────────────────────────────────────────────────────
async function recover() {
console.log('\n🚨 SYMBOL RECOVERY')
console.log('────────────────────────────────────────')
const SVG_DIR = path.join(__dirname, '..', 'public', 'signaturen')
if (!fs.existsSync(SVG_DIR)) {
console.error('❌ SVG-Verzeichnis nicht gefunden:', SVG_DIR)
process.exit(1)
}
const files = fs.readdirSync(SVG_DIR).filter(f => f.endsWith('.svg')).sort()
console.log(`\n📁 ${files.length} SVG-Dateien in public/signaturen/ gefunden`)
// ─── 1. Kategorien Upserten ───
console.log('\n[1/4] Kategorien...')
const catMap = {}
for (const def of catDefs) {
const catId = def.name.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()
if (IS_APPLY) {
const cat = await prisma.iconCategory.upsert({
where: { id: catId },
update: { name: def.name, description: def.description, sortOrder: def.sortOrder, isGlobal: true },
create: { id: catId, name: def.name, description: def.description, sortOrder: def.sortOrder, isGlobal: true, tenantId: null },
})
catMap[def.name] = cat.id
} else {
catMap[def.name] = catId
console.log(` 📝 ${def.name} (ID: ${catId})`)
}
}
if (IS_APPLY) console.log(`${catDefs.length} Kategorien upserted`)
// ─── 2. IconAssets Upserten ───
console.log('\n[2/4] IconAssets...')
const iconMap = {} // fileKey -> { id, name }
let created = 0
let updated = 0
for (const file of files) {
let name = file.replace('.svg', '')
name = name.replace(/_de$/i, '').replace(/_DE$/i, '').replace(/-de$/i, '')
name = name.replace(/[_-]/g, ' ').replace(/\s+/g, ' ').trim()
const fileKey = `signaturen/${file}` // WICHTIG: Gleiches Format wie Seed!
const category = findCategory(file)
const categoryId = catMap[category]
if (IS_APPLY) {
const existing = await prisma.iconAsset.findFirst({ where: { fileKey } })
if (existing) {
await prisma.iconAsset.update({
where: { id: existing.id },
data: { name, categoryId, mimeType: 'image/svg+xml', isSystem: true, isActive: true, width: 48, height: 48 },
})
iconMap[fileKey] = { id: existing.id, name }
updated++
} else {
const icon = await prisma.iconAsset.create({
data: { name, categoryId, fileKey, mimeType: 'image/svg+xml', isSystem: true, width: 48, height: 48 },
})
iconMap[fileKey] = { id: icon.id, name }
created++
}
} else {
// Dry-Run: Simuliere Lookup
const existing = await prisma.iconAsset.findFirst({ where: { fileKey }, select: { id: true, name: true } })
if (existing) {
iconMap[fileKey] = { id: existing.id, name: existing.name }
updated++
} else {
iconMap[fileKey] = { id: `(neu: ${fileKey})`, name }
created++
}
}
}
console.log(` ${IS_APPLY ? '✅' : '📝'} ${created} neu, ${updated} aktualisiert, ${files.length} total`)
// ─── 3. TenantSymbols erstellen ───
console.log('\n[3/4] TenantSymbols...')
const tenants = await prisma.tenant.findMany({ select: { id: true, name: true } })
let tsCreated = 0
let tsExisting = 0
for (const tenant of tenants) {
let tenantNew = 0
let tenantExist = 0
for (const [fileKey, icon] of Object.entries(iconMap)) {
if (IS_APPLY) {
// Prüfe ob bereits existiert
const existing = await prisma.tenantSymbol.findFirst({
where: { tenantId: tenant.id, iconId: icon.id },
})
if (existing) {
tenantExist++
tsExisting++
} else {
await prisma.tenantSymbol.create({
data: { tenantId: tenant.id, iconId: icon.id, isActive: true },
})
tenantNew++
tsCreated++
}
} else {
// Dry-Run: Prüfe Existenz
const existing = await prisma.tenantSymbol.findFirst({
where: { tenantId: tenant.id, iconId: icon.id },
select: { id: true },
})
if (existing) {
tenantExist++
tsExisting++
} else {
tenantNew++
tsCreated++
}
}
}
console.log(` ${IS_APPLY ? '✅' : '📝'} ${tenant.name}: ${tenantNew} neu, ${tenantExist} bereits vorhanden`)
}
console.log(` Gesamt: ${tsCreated} neu, ${tsExisting} bereits vorhanden`)
// ─── 4. Feature-Analyse ───
console.log('\n[4/4] Feature-Analyse (Zeichnungen)...')
const features = await prisma.feature.findMany({
where: { type: 'symbol' },
select: { id: true, properties: true, projectId: true },
})
const validIconIds = new Set(Object.values(iconMap).map(i => i.id))
let validRefs = 0
let brokenRefs = 0
for (const f of features) {
const iconId = f.properties?.iconId
if (!iconId) {
validRefs++ // Keine Referenz = nicht kaputt
} else if (validIconIds.has(iconId)) {
validRefs++
} else {
brokenRefs++
}
}
console.log(` 📊 ${features.length} symbol-Features gefunden`)
console.log(`${validRefs} gültige/keine Referenzen`)
console.log(`${brokenRefs} kaputte Referenzen (nicht reparierbar ohne Backup)`)
if (brokenRefs > 0) {
console.log('\n ⚠️ HINWEIS: Kaputte Features können NICHT automatisch repariert werden.')
console.log(' Die alten iconId-UUIDs sind verloren.')
console.log(' Verwende: npm run repair:features:dry-run')
}
console.log('\n✅ RECOVERY ABgeschlossen')
if (IS_DRY_RUN) {
console.log(' Zum Anwenden: node prisma/recover-symbols.js --apply')
}
}
recover()
.then(async () => { await prisma.$disconnect() })
.catch(async (e) => {
console.error('❌ Fehler:', e)
await prisma.$disconnect()
process.exit(1)
})

263
prisma/repair-features.js Normal file
View File

@@ -0,0 +1,263 @@
/**
* Feature/Symbol-Repair Skript
*
* Problem: Features (Zeichnungen) speichern Symbol-Referenzen als iconId in
* properties JSONB. Nach dem Icon-Verlust zeigen diese auf gelöschte UUIDs.
*
* Dieses Skript versucht:
* 1. Alle symbol-Features zu scannen
* 2. Zu prüfen, ob die referenzierte iconId noch existiert
* 3. Falls nicht: via migratedFromIconId oder Dateinamen-Matching zu reparieren
*
* Ausführung:
* Dry-Run (nur Analyse, keine Änderungen):
* node prisma/repair-features.js --dry-run
*
* Apply (mit Backup und Reparatur):
* node prisma/repair-features.js --apply
*
* SAFETY:
* - In Production (NODE_ENV=production) nur mit --apply möglich
* - Vor Apply wird automatisch ein JSON-Backup erstellt
* - Keine Features werden gelöscht
* - Keine Projekte oder Tenant-Daten werden verändert
*/
const { PrismaClient } = require('@prisma/client')
const fs = require('fs')
const path = require('path')
const prisma = new PrismaClient()
const IS_DRY_RUN = process.argv.includes('--dry-run')
const IS_APPLY = process.argv.includes('--apply')
const IS_PROD = process.env.NODE_ENV === 'production'
// ─── Safety Guards ─────────────────────────────────────────────────────────
if (!IS_DRY_RUN && !IS_APPLY) {
console.error('❌ Fehler: Bitte --dry-run oder --apply angeben')
console.error('')
console.error(' Analyse-Modus (keine Änderungen):')
console.error(' node prisma/repair-features.js --dry-run')
console.error('')
console.error(' Reparatur-Modus (mit Backup):')
console.error(' node prisma/repair-features.js --apply')
process.exit(1)
}
if (IS_DRY_RUN && IS_APPLY) {
console.error('❌ Fehler: --dry-run und --apply können nicht zusammen verwendet werden')
process.exit(1)
}
if (IS_APPLY) {
console.log('⚠️ REPAIR-MODUS (mit Änderungen)')
if (IS_PROD) {
console.log(' Production-Umgebung erkannt. Fortfahren in 3 Sekunden...')
await new Promise(r => setTimeout(r, 3000))
}
} else {
console.log('🔍 DRY-RUN MODUS (keine Änderungen)')
}
// ─── Statistics ────────────────────────────────────────────────────────────
const stats = {
totalFeatures: 0,
validRefs: 0,
brokenRefs: 0,
repaired: 0,
notRepairable: 0,
details: [],
}
// ─── Main ──────────────────────────────────────────────────────────────────
async function repair() {
console.log('\n🔧 Feature-Repair gestartet')
console.log('────────────────────────────────────────')
// ─── 1. Lade alle gültigen Icons für schnelle Lookup ───
console.log('\n[1/5] Lade Icon-Lookup-Tabellen...')
const allIcons = await prisma.iconAsset.findMany({
select: { id: true, name: true, fileKey: true },
})
const iconById = new Map(allIcons.map(i => [i.id, i]))
const iconByFileKey = new Map(allIcons.map(i => [i.fileKey, i]))
// tenant_symbols mit migratedFromIconId
const tenantSymbols = await prisma.tenantSymbol.findMany({
select: { id: true, iconId: true, migratedFromIconId: true, customName: true },
})
const tsByMigratedId = new Map()
for (const ts of tenantSymbols) {
if (ts.migratedFromIconId) {
tsByMigratedId.set(ts.migratedFromIconId, ts)
}
}
console.log(`${allIcons.length} IconAssets geladen`)
console.log(`${tenantSymbols.length} TenantSymbols geladen`)
// ─── 2. Lade alle symbol-Features ───
console.log('\n[2/5] Scanne Features...')
const features = await prisma.feature.findMany({
where: { type: 'symbol' },
include: { project: { select: { id: true, title: true, tenantId: true } } },
orderBy: { createdAt: 'asc' },
})
stats.totalFeatures = features.length
console.log(` 📊 ${features.length} symbol-Features gefunden`)
// ─── 3. Analysiere jede Feature ───
console.log('\n[3/5] Analysiere Symbol-Referenzen...')
const toRepair = []
for (const feature of features) {
const props = feature.properties || {}
const iconId = props.iconId
const imageUrl = props.imageUrl || ''
// Keine iconId → nicht reparierbar, aber auch nicht unbedingt kaputt
if (!iconId) {
stats.validRefs++ // Oder neutral
continue
}
// Prüfe ob iconId noch existiert
const icon = iconById.get(iconId)
if (icon) {
stats.validRefs++
continue
}
// ❌ Broken reference
stats.brokenRefs++
// Versuche Reparatur-Strategien
let repairedId = null
let repairMethod = null
// Strategie 1: via migratedFromIconId
const ts = tsByMigratedId.get(iconId)
if (ts && ts.iconId && iconById.has(ts.iconId)) {
repairedId = ts.iconId
repairMethod = 'migratedFromIconId'
}
// Strategie 2: via imageUrl (extrahiere Dateinamen)
if (!repairedId && imageUrl) {
const match = imageUrl.match(/\/api\/icons\/([^\/]+)\/image/)
if (match) {
// Die alte iconId war in der URL, aber die ist ja gelöscht
// Wir können hier nicht viel machen
}
}
// Strategie 3: via Name-Matching (wenn iconId ein bekannter Name war)
// Nicht anwendbar, da iconId eine UUID ist
if (repairedId) {
stats.repaired++
toRepair.push({
featureId: feature.id,
projectId: feature.projectId,
projectTitle: feature.project?.title || '(unbekannt)',
oldIconId: iconId,
newIconId: repairedId,
repairMethod,
properties: props,
})
} else {
stats.notRepairable++
stats.details.push({
featureId: feature.id,
projectId: feature.projectId,
projectTitle: feature.project?.title || '(unbekannt)',
iconId,
reason: 'Keine Zuordnung möglich (iconId nicht in migratedFromIconId)',
properties: JSON.stringify(props).substring(0, 200),
})
}
}
// ─── 4. Ergebnis-Anzeige ───
console.log('\n[4/5] Ergebnis-Zusammenfassung')
console.log('────────────────────────────────────────')
console.log(` Gesamt geprüfte Features: ${stats.totalFeatures}`)
console.log(` ✅ Gültige Referenzen: ${stats.validRefs}`)
console.log(` ❌ Kaputte Referenzen: ${stats.brokenRefs}`)
console.log(` 🔧 Automatisch reparierbar: ${stats.repaired}`)
console.log(` ⚠️ Nicht reparierbar: ${stats.notRepairable}`)
if (stats.details.length > 0) {
console.log('\n Nicht reparierbare Features (erste 10):')
for (const d of stats.details.slice(0, 10)) {
console.log(` - Feature ${d.featureId} in "${d.projectTitle}"`)
console.log(` iconId: ${d.iconId}`)
console.log(` Grund: ${d.reason}`)
}
if (stats.details.length > 10) {
console.log(` ... und ${stats.details.length - 10} weitere`)
}
}
// ─── 5. Apply (nur wenn --apply) ───
if (IS_APPLY && toRepair.length > 0) {
console.log('\n[5/5] Wende Reparaturen an...')
// Backup erstellen
const backupPath = path.join(__dirname, `feature-backup-${Date.now()}.json`)
const backup = toRepair.map(r => ({
featureId: r.featureId,
oldIconId: r.oldIconId,
newIconId: r.newIconId,
repairMethod: r.repairMethod,
oldProperties: r.properties,
}))
fs.writeFileSync(backupPath, JSON.stringify(backup, null, 2))
console.log(` 💾 Backup erstellt: ${backupPath}`)
// Reparaturen durchführen
let applied = 0
for (const r of toRepair) {
try {
const newProps = {
...r.properties,
iconId: r.newIconId,
}
await prisma.feature.update({
where: { id: r.featureId },
data: { properties: newProps },
})
applied++
console.log(` ✅ Feature ${r.featureId}: ${r.oldIconId}${r.newIconId} (${r.repairMethod})`)
} catch (e) {
console.error(` ❌ Feature ${r.featureId}: Update fehlgeschlagen - ${e.message}`)
}
}
console.log(`\n${applied}/${toRepair.length} Reparaturen erfolgreich angewendet`)
} else if (IS_APPLY) {
console.log('\n[5/5] Keine Reparaturen notwendig.')
} else {
console.log('\n[5/5] DRY-RUN abgeschlossen (keine Änderungen).')
if (toRepair.length > 0) {
console.log(` Zum Anwenden: node prisma/repair-features.js --apply`)
}
}
console.log('\n✅ Feature-Repair abgeschlossen')
}
repair()
.then(async () => { await prisma.$disconnect() })
.catch(async (e) => {
console.error('❌ Fehler:', e)
await prisma.$disconnect()
process.exit(1)
})

View File

@@ -74,6 +74,8 @@ model Tenant {
notes String?
hiddenIconIds String[] @default([])
journalSuggestions String[] @default([])
// Modul-Baukasten: Cockpit-Konfiguration (siehe src/lib/modules.ts). NULL = Standard.
modulesConfig Json?
// Privacy consent
privacyAccepted Boolean @default(false)
@@ -89,6 +91,8 @@ model Tenant {
checkTemplates JournalCheckTemplate[]
iconCategories IconCategory[]
iconAssets IconAsset[]
tenantSymbols TenantSymbol[]
tenantCategories TenantCategory[]
upgradeRequests UpgradeRequest[]
dictionaryEntries DictionaryEntry[]
rapports Rapport[]
@@ -116,10 +120,18 @@ model User {
name String
role Role @default(OPERATOR)
emailVerified Boolean @default(true)
// Super-Admin kann einzelne Benutzer sperren; gesperrte können sich nicht mehr anmelden.
isActive Boolean @default(true)
emailVerificationToken String? @unique
resetToken String? @unique
resetTokenExpiry DateTime?
lastLoginAt DateTime?
// ─── Zwei-Faktor-Authentisierung (MFA) ───
// TOTP (Authenticator-App): Secret gesetzt = Einrichtung begonnen; confirmedAt gesetzt = aktiv.
totpSecret String?
totpConfirmedAt DateTime?
// Einmalige Backup-Codes (bcrypt-gehasht), Fallback wenn kein Faktor verfügbar.
mfaBackupCodes String[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -128,10 +140,78 @@ model User {
iconAssets IconAsset[]
upgradeRequests UpgradeRequest[]
rapports Rapport[]
legalAcceptances LegalAcceptance[]
webauthnCredentials WebAuthnCredential[]
@@map("users")
}
// WebAuthn/FIDO2 (YubiKey, Passkeys, Touch-ID) — registrierte Sicherheitsschlüssel je Benutzer.
model WebAuthnCredential {
id String @id @default(uuid())
userId String
credentialId String @unique
publicKey String
counter Int @default(0)
transports String?
deviceName String?
createdAt DateTime @default(now())
lastUsedAt DateTime?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("webauthn_credentials")
}
// ─── Rechtsdokumente & revisionssichere Zustimmungen ──────────
// Dokumenttypen (als String gehalten, damit die idempotente Raw-SQL-Migration einfach bleibt):
// TERMS | PRIVACY | ORGANIZATION_DECLARATION | RESPONSIBLE_USE | DATA_PROCESSING_AGREEMENT
model LegalDocument {
id String @id @default(uuid())
type String
version String
title String
publishedAt DateTime @default(now())
effectiveAt DateTime?
contentHash String
url String?
isActive Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([type, version])
@@index([type, isActive])
@@map("legal_documents")
}
model LegalAcceptance {
id String @id @default(uuid())
userId String
// NULL = persönliche Zustimmung (nicht organisationsbezogen)
organizationId String?
documentType String
documentVersion String
contentHash String
acceptedAt DateTime @default(now())
locale String?
// REGISTRATION | LOGIN_RECONSENT | ORGANIZATION_CREATION | ADMIN_ROLE_ACCEPTANCE | DONATION
context String
// Nachweisdaten (zu Beweiszwecken der Einwilligung): IP + Gerät/Browser zum Zeitpunkt der Zustimmung.
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Verhindert doppelte identische Zustimmungen; neue Version erzeugt neuen Datensatz.
@@unique([userId, organizationId, documentType, documentVersion, context])
@@index([userId])
@@index([documentType, documentVersion])
@@map("legal_acceptances")
}
model TenantMembership {
id String @id @default(uuid())
role Role @default(OPERATOR)
@@ -149,6 +229,11 @@ model TenantMembership {
model Project {
id String @id @default(uuid())
einsatzNr String?
// "EINSATZ" = echter Einsatz (mit Journal), "UEBUNG" = Übung (Cockpit statt Journal).
// Als String (nicht Enum) gehalten, damit die idempotente Raw-SQL-Migration einfach bleibt.
mode String @default("EINSATZ")
// Nur für Übungen: Gesamt-Auswertung / Lessons Learned (Freitext).
exerciseEvaluation String?
title String
location String?
description String?
@@ -156,7 +241,19 @@ model Project {
journalfuehrer String?
mapCenter Json @default("{\"lng\": 8.5417, \"lat\": 47.3769}")
mapZoom Float @default(15)
// Windrichtung in Grad (meteorologisch „Wind aus"), fürs Lagebild. NULL = nicht gesetzt.
windDirection Int?
isLocked Boolean @default(false)
// Optimistische Nebenläufigkeit: wird bei jedem Features-Speichern hochgezählt.
// Verhindert, dass ein veralteter (z.B. offline zwischengespeicherter) Stand
// neuere Änderungen stillschweigend überschreibt.
featuresVersion Int @default(0)
// Öffentlicher Nur-Ansicht-Teilen-Link (live). shareToken = zufälliger Slug im Link,
// sharePin = bcrypt-Hash einer optionalen PIN (NULL = keine PIN), shareEnabled = aktiv/aus.
shareToken String? @unique
sharePin String?
shareEnabled Boolean @default(false)
// Live editing lock (session-based for same-account multi-device)
editingById String?
@@ -178,11 +275,32 @@ model Project {
journalEntries JournalEntry[]
journalCheckItems JournalCheckItem[]
journalPendenzen JournalPendenz[]
exerciseGoals ExerciseGoal[]
rapports Rapport[]
moduleItems ModuleItem[]
@@map("projects")
}
// ─── Übungs-Cockpit: Übungsziele mit Zielerreichung ──────────
model ExerciseGoal {
id String @id @default(uuid())
text String
// Zielerreichung: OPEN | REACHED | PARTIAL | MISSED
status String @default("OPEN")
note String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@index([projectId])
@@map("exercise_goals")
}
model Feature {
id String @id @default(uuid())
type String
@@ -236,6 +354,8 @@ model IconAsset {
tenantId String?
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull)
tenantSymbols TenantSymbol[]
@@map("icon_assets")
}
@@ -314,6 +434,24 @@ model JournalPendenz {
@@map("journal_pendenzen")
}
// Generischer Datenspeicher für Tabellen-Module (Modul-Baukasten).
// data enthält die Zellwerte gemäss Spaltendefinition des Moduls,
// z.B. { "trupp": "Trupp 1", "druck": "280", "out": true, "outAt": "..." }
model ModuleItem {
id String @id @default(uuid())
moduleId String
data Json @default("{}")
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@index([projectId, moduleId])
@@map("module_items")
}
model JournalCheckTemplate {
id String @id @default(uuid())
label String
@@ -375,6 +513,67 @@ model UpgradeRequest {
@@map("upgrade_requests")
}
// ─── Tenant Symbol Collection ─────────────────────────────
model TenantSymbol {
id String @id @default(uuid())
customName String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
iconId String?
icon IconAsset? @relation(fields: [iconId], references: [id], onDelete: SetNull)
// New fields for Phase 1 Symbol Architecture
categoryId String?
category TenantCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
name String? // Display name (migrated from customName || icon.name)
svgPath String? // e.g. "signaturen/TLF.svg" or tenant-specific MinIO key
isUploaded Boolean @default(false)
migratedFromIconId String?
@@index([tenantId])
@@index([categoryId])
@@map("tenant_symbols")
}
// ─── Symbol Templates (global read-only packages) ─────────
model SymbolTemplate {
id String @id @default(uuid())
packageId String // e.g. "feuerwehr-ch"
packageName String // e.g. "Feuerwehr Schweiz"
categoryName String // e.g. "Fahrzeuge"
name String
svgPath String // relative path in public/ or MinIO key
tags String[] @default([])
sortOrder Int @default(0)
@@index([packageId])
@@map("symbol_templates")
}
// ─── Tenant Categories (per-tenant, user-managed) ─────────
model TenantCategory {
id String @id @default(uuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
name String
sortOrder Int @default(0)
icon String? // Optional emoji or Lucide icon name for UI
symbols TenantSymbol[]
@@unique([tenantId, name])
@@index([tenantId])
@@map("tenant_categories")
}
// ─── Dictionary (Global + Tenant word library) ────────────
model DictionaryEntry {

View File

@@ -44,11 +44,10 @@ async function main() {
patterns: ['Massstab', 'Nordrichtung', 'Windrichtung'] },
]
// Delete ALL old system icons (regardless of fileKey pattern)
const deleted = await prisma.iconAsset.deleteMany({
where: { isSystem: true },
})
console.log(`🗑️ ${deleted.count} old system icons removed`)
// NOTE: We intentionally do NOT delete old system icons here.
// TenantSymbol rows reference IconAsset.id via foreign key.
// Deleting would either break references (tenant symbols become 404s)
// or cascade-delete tenant symbols. Instead we upsert by fileKey.
// Upsert global categories (preserves tenant categories)
const catMap = {}
@@ -90,6 +89,7 @@ async function main() {
}
let created = 0
let updated = 0
for (const file of svgFiles) {
let name = file.replace('.svg', '')
name = name.replace(/_de$/i, '').replace(/_DE$/i, '').replace(/-de$/i, '')
@@ -99,7 +99,21 @@ async function main() {
const category = findCategory(file)
const existing = await prisma.iconAsset.findFirst({ where: { fileKey } })
if (!existing) {
if (existing) {
await prisma.iconAsset.update({
where: { id: existing.id },
data: {
name,
categoryId: category.id,
mimeType: 'image/svg+xml',
isSystem: true,
isActive: true,
width: 48,
height: 48,
},
})
updated++
} else {
await prisma.iconAsset.create({
data: {
name,
@@ -115,7 +129,7 @@ async function main() {
}
}
console.log(`✅ FKS Signaturen: ${created} new SVG icons created (${svgFiles.length} total)`)
console.log(`✅ FKS Signaturen: ${created} created, ${updated} updated (${svgFiles.length} total)`)
}
main()

View File

@@ -0,0 +1,123 @@
/**
* Seed-Skript: Erstellt SymbolTemplate-Einträge aus bestehenden IconAsset (isSystem=true)
* oder aus dem Dateisystem (public/signaturen/*.svg).
*
* Ausführen:
* npx ts-node prisma/seed-symbol-templates.ts
* oder als Teil des Deployments via npx prisma db seed
*
* Idempotent: bereits existierende Templates werden übersprungen.
*/
import { PrismaClient } from '@prisma/client'
import { readdirSync, statSync } from 'fs'
import { join } from 'path'
const prisma = new PrismaClient()
const SIGNATUREN_DIR = join(process.cwd(), 'public', 'signaturen')
const PACKAGE_ID = 'feuerwehr-ch'
const PACKAGE_NAME = 'Feuerwehr Schweiz'
/**
* Versucht Kategorie-Namen aus bestehenden IconAsset / IconCategory abzuleiten.
* Fallback: "Sonstiges"
*/
async function getCategoryMapping(): Promise<Map<string, string>> {
const mapping = new Map<string, string>()
const iconAssets = await (prisma as any).iconAsset.findMany({
where: { isSystem: true },
include: { category: { select: { name: true } } },
})
for (const asset of iconAssets) {
const fileName = asset.fileKey.replace(/^signaturen\//, '').replace(/\.svg$/i, '')
const categoryName = asset.category?.name || 'Sonstiges'
mapping.set(fileName.toLowerCase(), categoryName)
}
return mapping
}
/**
* Liest alle .svg Dateien aus public/signaturen/
*/
function getSvgFiles(dir: string): string[] {
const files: string[] = []
try {
const entries = readdirSync(dir)
for (const entry of entries) {
const fullPath = join(dir, entry)
const stat = statSync(fullPath)
if (stat.isFile() && entry.endsWith('.svg')) {
files.push(entry)
}
}
} catch (err) {
console.warn('Could not read signaturen directory:', (err as Error).message)
}
return files.sort()
}
async function main() {
console.log('🌱 Seeding SymbolTemplate...')
const categoryMapping = await getCategoryMapping()
const svgFiles = getSvgFiles(SIGNATUREN_DIR)
console.log(`Found ${svgFiles.length} SVG files in public/signaturen/`)
console.log(`IconAsset mapping covers ${categoryMapping.size} entries`)
let created = 0
let skipped = 0
for (let i = 0; i < svgFiles.length; i++) {
const fileName = svgFiles[i]
const nameWithoutExt = fileName.replace(/\.svg$/i, '')
const displayName = nameWithoutExt
.replace(/_/g, ' ')
.replace(/-/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
const fileKey = `signaturen/${fileName}`
// Check if already exists
const existing = await (prisma as any).symbolTemplate.findFirst({
where: { svgPath: fileKey },
})
if (existing) {
skipped++
continue
}
const categoryName =
categoryMapping.get(nameWithoutExt.toLowerCase()) || 'Sonstiges'
await (prisma as any).symbolTemplate.create({
data: {
packageId: PACKAGE_ID,
packageName: PACKAGE_NAME,
categoryName,
name: displayName,
svgPath: fileKey,
tags: [nameWithoutExt.toLowerCase(), categoryName.toLowerCase()],
sortOrder: i,
},
})
created++
}
console.log(`✅ Done: ${created} created, ${skipped} skipped`)
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})

View File

@@ -110,21 +110,15 @@ async function main() {
patterns: ['Massstab', 'Nordrichtung', 'Windrichtung'] },
]
// Delete ALL old system icons (regardless of fileKey pattern)
const deletedIcons = await prisma.iconAsset.deleteMany({
where: { isSystem: true },
})
console.log(`🗑️ ${deletedIcons.count} old system icons removed`)
// NOTE: We intentionally do NOT delete old system icons here.
// TenantSymbol rows reference IconAsset.id via foreign key.
// Deleting would either break references (tenant symbols become 404s)
// or cascade-delete tenant symbols. Instead we upsert by fileKey.
// Clean up empty global categories
const oldGlobalCats = await prisma.iconCategory.findMany({ where: { tenantId: null } })
for (const oldCat of oldGlobalCats) {
const remaining = await prisma.iconAsset.count({ where: { categoryId: oldCat.id } })
if (remaining === 0) {
await prisma.iconCategory.delete({ where: { id: oldCat.id } }).catch(() => {})
}
}
// Create new global categories
// NOTE: We intentionally do NOT delete any icon categories here.
// Tenant-specific categories may reference them, and deleting could
// orphan user data. Empty categories are harmless.
// Create or update global categories
const catMap = {}
for (const def of catDefs) {
const cat = await prisma.iconCategory.upsert({
@@ -163,6 +157,7 @@ async function main() {
}
let created = 0
let updated = 0
for (const file of svgFiles) {
// Clean name: remove .svg, remove _de/_DE suffix
let name = file.replace('.svg', '')
@@ -173,7 +168,21 @@ async function main() {
const category = findCategory(file)
const existing = await prisma.iconAsset.findFirst({ where: { fileKey } })
if (!existing) {
if (existing) {
await prisma.iconAsset.update({
where: { id: existing.id },
data: {
name,
categoryId: category.id,
mimeType: 'image/svg+xml',
isSystem: true,
isActive: true,
width: 48,
height: 48,
},
})
updated++
} else {
await prisma.iconAsset.create({
data: {
name,
@@ -189,7 +198,7 @@ async function main() {
}
}
console.log(`✅ FKS Signaturen: ${created} new icons created (${svgFiles.length} total SVGs)`)
console.log(`✅ FKS Signaturen: ${created} created, ${updated} updated (${svgFiles.length} total SVGs)`)
// Create a demo project
const demoProject = await prisma.project.upsert({
@@ -252,32 +261,8 @@ async function main() {
console.log('✅ Hose types created:', hoseTypes.length)
// ─── Journal Check Templates (SOMA) ─────────────────────
const somaTemplates = [
{ label: 'Stopp Zutritt / Ex-Gefahr', sortOrder: 1 },
{ label: 'Alarmierung Gesamt-Fw', sortOrder: 2 },
{ label: 'Alarmierung Ofw Wohlen', sortOrder: 3 },
{ label: 'Alarmierung Stp Baden', sortOrder: 4 },
{ label: 'Alarmierung Ambulanz', sortOrder: 5 },
{ label: 'Alarmierung BWL', sortOrder: 6 },
{ label: 'Alarmierung BL/Meister', sortOrder: 7 },
{ label: 'Brunnenchef Villmergen', sortOrder: 8 },
{ label: 'Berieselung Tank / Anl', sortOrder: 9 },
{ label: 'Druckerhöhung', sortOrder: 10 },
]
for (const tpl of somaTemplates) {
await prisma.journalCheckTemplate.upsert({
where: { id: tpl.label.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase() },
update: { label: tpl.label, sortOrder: tpl.sortOrder },
create: {
id: tpl.label.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase(),
label: tpl.label,
sortOrder: tpl.sortOrder,
},
})
}
console.log('✅ SOMA templates created:', somaTemplates.length)
// No default SOMA templates — each tenant defines their own via Admin → SOMA tab
console.log(' SOMA templates: keine Standard-Vorgaben (Tenants konfigurieren eigene)')
console.log('🎉 Seed completed successfully!')
}

View File

@@ -1,4 +1,4 @@
import { PrismaClient, Role } from '@prisma/client'
import { PrismaClient } from '@prisma/client'
import bcrypt from 'bcryptjs'
const prisma = new PrismaClient()
@@ -18,7 +18,7 @@ async function main() {
email: 'admin@lageplan.local',
name: 'Administrator',
password: adminPassword,
role: Role.ADMIN,
role: 'SERVER_ADMIN',
},
})
@@ -29,7 +29,7 @@ async function main() {
email: 'editor@lageplan.local',
name: 'Einsatzleiter',
password: editorPassword,
role: Role.EDITOR,
role: 'TENANT_ADMIN',
},
})
@@ -40,28 +40,28 @@ async function main() {
email: 'viewer@lageplan.local',
name: 'Beobachter',
password: viewerPassword,
role: Role.VIEWER,
role: 'OPERATOR',
},
})
console.log('✅ Users created:', { admin: admin.email, editor: editor.email, viewer: viewer.email })
// Create icon categories
// Create icon categories with stable IDs (same logic as seed.js)
const categories = [
{ name: 'Feuer', description: 'Brand- und Feuersymbole', sortOrder: 1 },
{ name: 'Wasser', description: 'Wasser- und Überflutungssymbole', sortOrder: 2 },
{ name: 'Gefahrstoffe', description: 'Chemie- und Gefahrstoffsymbole', sortOrder: 3 },
{ name: 'Verkehr', description: 'Verkehrs- und Unfallsymbole', sortOrder: 4 },
{ name: 'Personen', description: 'Personen- und Rettungssymbole', sortOrder: 5 },
{ name: 'Fahrzeuge', description: 'Einsatzfahrzeuge und Geräte', sortOrder: 6 },
{ name: 'Infrastruktur', description: 'Gebäude und Infrastruktursymbole', sortOrder: 7 },
{ name: 'Taktik', description: 'Taktische Zeichen und Symbole', sortOrder: 8 },
{ name: 'Eigene', description: 'Benutzerdefinierte Symbole', sortOrder: 99 },
{ id: 'feuer', name: 'Feuer', description: 'Brand- und Feuersymbole', sortOrder: 1 },
{ id: 'wasser', name: 'Wasser', description: 'Wasser- und Überflutungssymbole', sortOrder: 2 },
{ id: 'gefahrstoffe', name: 'Gefahrstoffe', description: 'Chemie- und Gefahrstoffsymbole', sortOrder: 3 },
{ id: 'verkehr', name: 'Verkehr', description: 'Verkehrs- und Unfallsymbole', sortOrder: 4 },
{ id: 'personen', name: 'Personen', description: 'Personen- und Rettungssymbole', sortOrder: 5 },
{ id: 'fahrzeuge', name: 'Fahrzeuge', description: 'Einsatzfahrzeuge und Geräte', sortOrder: 6 },
{ id: 'infrastruktur', name: 'Infrastruktur', description: 'Gebäude und Infrastruktursymbole', sortOrder: 7 },
{ id: 'taktik', name: 'Taktik', description: 'Taktische Zeichen und Symbole', sortOrder: 8 },
{ id: 'eigene', name: 'Eigene', description: 'Benutzerdefinierte Symbole', sortOrder: 99 },
]
for (const cat of categories) {
await prisma.iconCategory.upsert({
where: { name: cat.name },
where: { id: cat.id },
update: { description: cat.description, sortOrder: cat.sortOrder },
create: cat,
})
@@ -70,14 +70,14 @@ async function main() {
console.log('✅ Icon categories created:', categories.length)
// Get category IDs for system icons
const feuerCat = await prisma.iconCategory.findUnique({ where: { name: 'Feuer' } })
const wasserCat = await prisma.iconCategory.findUnique({ where: { name: 'Wasser' } })
const gefahrstoffeCat = await prisma.iconCategory.findUnique({ where: { name: 'Gefahrstoffe' } })
const verkehrCat = await prisma.iconCategory.findUnique({ where: { name: 'Verkehr' } })
const personenCat = await prisma.iconCategory.findUnique({ where: { name: 'Personen' } })
const fahrzeugeCat = await prisma.iconCategory.findUnique({ where: { name: 'Fahrzeuge' } })
const infrastrukturCat = await prisma.iconCategory.findUnique({ where: { name: 'Infrastruktur' } })
const taktikCat = await prisma.iconCategory.findUnique({ where: { name: 'Taktik' } })
const feuerCat = await prisma.iconCategory.findUnique({ where: { id: 'feuer' } })
const wasserCat = await prisma.iconCategory.findUnique({ where: { id: 'wasser' } })
const gefahrstoffeCat = await prisma.iconCategory.findUnique({ where: { id: 'gefahrstoffe' } })
const verkehrCat = await prisma.iconCategory.findUnique({ where: { id: 'verkehr' } })
const personenCat = await prisma.iconCategory.findUnique({ where: { id: 'personen' } })
const fahrzeugeCat = await prisma.iconCategory.findUnique({ where: { id: 'fahrzeuge' } })
const infrastrukturCat = await prisma.iconCategory.findUnique({ where: { id: 'infrastruktur' } })
const taktikCat = await prisma.iconCategory.findUnique({ where: { id: 'taktik' } })
// Create system icons (these use inline SVG data URIs - in production, upload to MinIO)
const systemIcons = [

BIN
public/Pepe_Avatar.mp4 Normal file

Binary file not shown.

73
public/signaturen/ADL.svg Normal file
View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="75.489105"
height="38.344212"
viewBox="0 0 75.489106 38.344212"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-4.0682266,-9.1345345)">
<path
id="path1"
d="M 591.68,76.1094 H 35.5117 V 353.691 H 591.68 Z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 427.379,76.1094 V 353.691"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 427.379,352.969 588.219,216.59 427.379,75.5117"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 35.5117,276.059 H 427.379"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="M 35.5117,149.352 H 427.379"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="M 91.7813,149.352 V 276.059"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="M 150.801,149.352 V 276.059"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path8"
d="M 209.82,149.352 V 276.059"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path9"
d="M 271.18,149.352 V 276.059"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path10"
d="M 330.18,149.352 V 276.059"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path11"
d="M 389.219,149.352 V 276.059"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="74.508385"
height="39.839844"
viewBox="0 0 74.508385 39.839844"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-4.6873163,-8.4297528)">
<path
id="path1"
d="M 586.551,175.039 457.031,322.93 127.309,358.73 41.5898,263.578 144.281,123.23 438.648,70.4688 Z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 778 B

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="78.721375"
height="32.409245"
viewBox="0 0 78.721375 32.409244"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-2.4348933,-11.840344)">
<path
id="path1"
d="M 23.2617,260.91 H 603.672"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 23.2617,193.762 H 603.672"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 261.871,323.91 21.039,9.969"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 226.238,141.738 -26.347,13.742 v 136.411 l 32.73,15.5"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="M 284.609,110.34 257.07,124.711"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="m 212.84,147.961 65.969,0.23 -26.207,-52.8629 -39.762,52.6329"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="m 212.84,147.961 65.969,0.23 -26.207,-52.8629 z m 20.031,-9.93 29.789,0.11 -11.832,-23.879 -17.957,23.769"
style="fill:#00adef;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path8"
d="m 276.891,331.641 -39.18,-53.082 -26.789,52.582 65.969,0.5"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path9"
d="m 276.891,331.641 -39.18,-53.082 -26.789,52.582 z m -19.93,-10.153 -17.691,-23.968 -12.098,23.742 29.789,0.226"
style="fill:#00adef;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="49.4916"
height="45.740582"
viewBox="0 0 49.4916 45.740581"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-16.928133,-4.8954568)">
<path
id="path1"
d="M 474.441,139.84 H 150.68 L 311.93,381.5 Z"
style="fill:none;stroke:#f47216;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 314.43,180.762 V 325.25"
style="fill:none;stroke:#f47216;stroke-width:30;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 126.961,63.3984 h 13.379 v 17.543 h 5.18 c 12.511,0 19.91,6.0391 19.91,16.1797 0,11.2889 -7.192,16.4689 -22.5,16.4689 h -15.969 z m 13.379,40.7036 h 3.449 c 4.602,0 8.27,-1.661 8.27,-6.7622 0,-5.25 -3.668,-6.9101 -8.27,-6.9101 h -3.449 v 13.6723"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 170.969,63.3984 h 12.941 v 15.8204 c 0,8.0507 2.738,11.7226 8.059,11.7226 3.519,0 5.972,-2.3828 5.972,-9.4219 V 63.3984 h 12.938 v 23.3711 c 0,7.7618 -4.098,14.9495 -14.309,14.9495 -5.898,0 -11.07,-2.3674 -13.66,-7.7581 h -0.148 v 6.8981 H 170.969 V 63.3984"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="m 256.109,78.3516 v 2.3789 c 0,11.1406 -5.039,20.9885 -18.91,20.9885 -11.648,0 -20.07,-7.1877 -20.07,-19.5588 0,-12.3594 9.422,-19.6289 21.793,-19.6289 4.816,0 9.566,0.7187 13.738,2.3789 v 9.1289 c -3.89,-2.0899 -7.91,-2.8789 -11.441,-2.8789 -6.321,0 -10.278,2.0195 -11,7.1914 z m -26.031,7.3398 c 0.293,4.3086 2.512,7.8281 7.192,7.8281 5.179,0 7.191,-3.5195 7.191,-7.8281 h -14.383"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="M 302.781,100.859 H 289.828 V 85.0391 c 0,-8.0508 -2.726,-11.7188 -8.047,-11.7188 -3.519,0 -5.972,2.3711 -5.972,9.418 V 100.859 H 262.871 V 77.4883 c 0,-7.7695 4.098,-14.957 14.309,-14.957 5.89,0 11.07,2.3789 13.66,7.7695 h 0.14 v -6.9024 h 11.801 v 37.4606"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="m 312.34,63.3984 h 12.941 V 117.328 H 312.34 V 63.3984"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path8"
d="m 371.23,63.3984 c -0.57,2.9414 -0.718,5.8907 -0.718,8.8399 v 13.9531 c 0,11.4297 -8.274,15.5276 -17.832,15.5276 -5.539,0 -10.36,-0.789 -14.961,-2.6604 l 0.222,-8.7695 c 3.59,2.0117 7.758,2.8007 11.93,2.8007 4.68,0 8.488,-1.3593 8.559,-6.4687 -1.649,0.2891 -3.95,0.5078 -6.039,0.5078 -6.903,0 -19.34,-1.3672 -19.34,-12.8008 0,-8.1289 6.609,-11.7968 14.019,-11.7968 5.321,0 8.918,2.0898 11.871,6.7578 h 0.137 c 0,-1.9375 0.211,-3.8789 0.293,-5.8907 z m -26.101,11.8633 c 0,3.5977 3.449,4.9571 7.84,4.9571 1.941,0 3.812,-0.1407 5.461,-0.2071 0,-4.3906 -3.09,-8.8515 -7.981,-8.8515 -3.019,0 -5.32,1.5117 -5.32,4.1015"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path9"
d="M 418.469,100.859 H 406.75 v -6.7574 h -0.141 c -2.73,4.5976 -7.187,7.6174 -12.73,7.6174 -11.649,0 -16.899,-8.3401 -16.899,-19.4807 0,-11.0781 6.399,-18.8399 16.692,-18.8399 5.168,0 9.488,2.0118 12.578,6.4024 h 0.141 v -2.0195 c 0,-7.3321 -4.02,-10.8594 -11.5,-10.8594 -5.391,0 -8.7,1.1484 -12.661,3.0976 l -0.64,-10.2109 c 3.019,-1.0781 8.121,-2.3789 14.308,-2.3789 15.102,0 22.571,4.9609 22.571,20.3516 z m -28.11,-18.4098 c 0,5.1094 2.52,9.3516 7.332,9.3516 5.829,0 8.27,-4.6719 8.27,-8.8516 0,-5.75 -3.672,-9.6289 -8.27,-9.6289 -3.882,0 -7.332,3.3086 -7.332,9.1289"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path10"
d="m 463.988,78.3516 v 2.3789 c 0,11.1406 -5.027,20.9885 -18.91,20.9885 -11.648,0 -20.058,-7.1877 -20.058,-19.5588 0,-12.3594 9.421,-19.6289 21.789,-19.6289 4.812,0 9.562,0.7187 13.73,2.3789 v 9.1289 c -3.879,-2.0899 -7.91,-2.8789 -11.43,-2.8789 -6.328,0 -10.289,2.0195 -11.007,7.1914 z m -26.027,7.3398 c 0.289,4.3086 2.519,7.8281 7.187,7.8281 5.18,0 7.192,-3.5195 7.192,-7.8281 h -14.379"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path11"
d="m 498.148,101.289 c -1.296,0.223 -2.878,0.43 -4.386,0.43 -5.614,0 -8.84,-3.0198 -11.071,-7.7581 h -0.152 v 6.8981 H 470.75 V 63.3984 h 12.941 v 15.8204 c 0,7.332 3.379,11.7226 9.418,11.7226 1.512,0 2.95,0 4.391,-0.4414 l 0.648,10.789"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="28.357332"
height="48.064117"
viewBox="0 0 28.357332 48.064117"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-27.697333,-4.490668)">
<path
id="path1"
d="M 212.73,33.0391 V 178.07"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 415.41,33.0391 V 178.07"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 241.559,39.1016 V 393.52"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 386.59,39.1016 V 393.52"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="M 241.559,84.7891 H 386.59"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="M 241.559,142.621 H 386.59"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="M 241.559,199.879 H 386.59"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path8"
d="M 241.559,257.172 H 386.59"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path9"
d="M 241.559,313.32 H 386.59"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path10"
d="M 241.559,369.469 H 386.59"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="77.460358"
height="15.1172"
viewBox="0 0 77.460358 15.1172"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-3.0651066,-20.964134)">
<path
id="path1"
d="M 22.9883,264.969 H 603.941"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 22.9883,161.59 H 603.941"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 369.191,213.57 H 482.898"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 22.9883,213.57 H 186.539"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="m 476.672,187.48 v 52.18 l 99.297,-25.699 -99.297,-26.481"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="m 223.789,188.98 h 10.09 v 22.04 h 17 v 7.921 h -17 v 12.387 h 17.859 v 7.922 h -27.949 v -50.27"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="m 259.809,188.98 h 29.82 v 7.918 h -19.738 v 14.122 h 17.937 v 7.921 h -17.937 v 12.387 h 19.589 v 7.922 h -29.671 v -50.27"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path8"
d="m 299.5,207.129 c 0,-13.469 7.629,-19.02 20.52,-19.02 12.902,0 20.531,5.551 20.531,19.02 v 32.121 h -10.082 v -29.031 c 0,-8.571 -2.739,-14.188 -10.449,-14.188 -7.7,0 -10.442,5.617 -10.442,14.188 V 239.25 H 299.5 v -32.121"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="19.9772"
height="40.640144"
viewBox="0 0 19.9772 40.640144"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-31.830666,-8.2026679)">
<path
id="path1"
d="M 383.559,60.8789 V 365.68"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 243.73,60.8789 V 365.68"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 243.73,102 H 383.559"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 243.73,150.23 H 383.559"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="M 243.73,199.559 H 383.559"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="M 243.73,248.879 H 383.559"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="M 243.73,296.211 H 383.559"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path8"
d="M 243.73,347.539 H 383.559"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="73.458839"
height="43.304184"
viewBox="0 0 73.45884 43.304184"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-4.7239599,-6.5370679)">
<path
id="path1"
d="M 40.4297,53.3906 V 373.172 H 586.371"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 313.93,373.172 V 235.051 H 40.4297"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 97.4297,293.531 v 9.098 h -21.75 v -9.098 h 21.75"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 106.871,320 5.789,-7.52 9.762,7.942 V 275.828 H 134 V 333.57 H 123.738 L 106.871,320"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="m 148.07,274.828 h 8.602 l 20.187,59.731 h -8.609 l -20.18,-59.731"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="M 199.031,301.059 H 182.32 v -8.61 h 16.711 v -16.621 h 8.598 v 16.621 h 16.711 v 8.61 h -16.711 v 16.632 h -8.598 v -16.632"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="m 231.871,276.738 c 4.301,-1.25 10.098,-1.91 15.141,-1.91 9.597,0 22.09,3.481 22.09,17.211 0,7.031 -3.641,12.742 -11,13.731 v 0.171 c 5.867,2.231 10.75,5.957 10.75,13.821 0,12.316 -11.911,14.797 -19.032,14.797 -5.949,0 -11.5,-1.071 -16.129,-2.887 l 0.661,-9.274 c 4.058,1.993 8.437,3.063 12.988,3.063 5.211,0 9.93,-1.82 9.93,-7.609 0,-5.043 -5.051,-7.614 -12.821,-7.614 h -5.301 v -9.097 h 5.633 c 7.36,0 12.739,-2.231 12.739,-9.02 0,-5.871 -7.36,-8.191 -12.161,-8.191 -4.55,0 -8.859,1.082 -12.828,2.648 l -0.66,-9.84"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="56.927597"
height="37.361401"
viewBox="0 0 56.927597 37.361401"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-13.168266,-9.8626678)">
<path
id="path1"
d="M 520.719,78.0195 H 103.762 V 348.23 h 416.957 z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 273.301,177.449 h 19.32 l 5.617,15.231 h 26.782 l 5.308,-15.231 h 20.852 l -27.489,71.34 h -23.3 z m 47.219,28.723 h -17.891 l 9.199,27.906 h 0.211 l 8.481,-27.906"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="78.605675"
height="40.806187"
viewBox="0 0 78.605675 40.806187"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-2.3864533,-8.1402679)">
<path
id="path1"
d="M 408.422,65.1016 V 361.148"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 287.012,65.1016 V 361.148"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 602.441,217.969 H 22.8984"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 554.59,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="m 522.898,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="m 493.102,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="m 463.379,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path8"
d="m 433.66,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path9"
d="m 347.949,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path10"
d="m 318.238,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path11"
d="m 378.039,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path12"
d="m 222.172,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path13"
d="m 192.449,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path14"
d="m 252.262,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path15"
d="m 130.359,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path16"
d="m 100.648,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path17"
d="m 70.1406,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path18"
d="m 160.461,170.66 v 92.399"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="79.002655"
height="31.690386"
viewBox="0 0 79.002655 31.690386"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-2.1161466,-12.698134)">
<path
id="path1"
d="M 271.629,99.3711 V 326.879"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 352.641,99.3711 V 326.879"
style="fill:none;stroke:#231f20;stroke-width:10.17;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 257.691,162.391 h -51.382 l -52.75,-59.629"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 257.691,263.871 H 206.309 L 153.559,323.5"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="m 365.449,162.391 h 51.371 l 52.75,-59.629"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="M 365.449,263.871 H 416.82 L 469.57,323.5"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="m 352.641,213.121 h 250.75"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path8"
d="m 388.719,178.109 v 70.032"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path9"
d="m 413.98,178.109 v 70.032"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path10"
d="m 439.219,178.109 v 70.032"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path11"
d="m 464.469,178.109 v 70.032"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path12"
d="m 489.719,178.109 v 70.032"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path13"
d="m 514.969,178.109 v 70.032"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path14"
d="m 540.23,178.109 v 70.032"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path15"
d="m 565.469,178.109 v 70.032"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path16"
d="M 271.629,213.121 H 20.8711"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path17"
d="M 235.551,248.141 V 178.109"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path18"
d="M 210.289,248.141 V 178.109"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path19"
d="M 185.051,248.141 V 178.109"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path20"
d="M 159.789,248.141 V 178.109"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path21"
d="M 134.539,248.141 V 178.109"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path22"
d="M 109.289,248.141 V 178.109"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path23"
d="M 84.0391,248.141 V 178.109"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path24"
d="M 58.8008,248.141 V 178.109"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="80.705238"
height="42.09684"
viewBox="0 0 80.705238 42.09684"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-1.2348933,-7.4949346)">
<path
id="path1"
d="M 14.2617,215.441 H 609.551"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 153.23,106.379 55.008,55.012 H 422.98 l 63.7,-63.6996"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 160.469,315.809 54.769,-54.77 H 422.5 l 54.879,54.891"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 114.922,175.762 V 245.25"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="M 176.199,175.762 V 245.25"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="M 238.16,175.762 V 245.25"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="M 299.93,175.762 V 245.25"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path8"
d="M 354,175.762 V 245.25"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path9"
d="M 408.078,175.762 V 245.25"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path10"
d="M 469.852,175.762 V 245.25"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path11"
d="M 523.922,175.762 V 245.25"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path12"
d="M 260.84,261.039 V 365.988"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path13"
d="M 345.77,261.039 V 365.988"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path14"
d="M 260.84,60.2617 V 161.391"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path15"
d="M 345.77,60.2617 V 161.391"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="60.768242"
height="47.310577"
viewBox="0 0 60.768242 47.310577"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-11.286615,-4.6015026)">
<path
id="path1"
d="M 536.262,42.8594 H 88.8594 L 307.199,383.578 Z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 232.422,125.23 h 20.027 v 30.258 h 28.981 v 14.223 h -28.981 v 16.48 h 30.27 v 14.219 h -50.297 v -75.18"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 303.398,125.23 h 25.961 l 9.911,57.731 h 0.21 l 10.661,-57.731 h 25.75 l 16.8,75.18 h -19.5 l -9.8,-57.73 h -0.211 l -10.559,57.73 h -25.422 l -10.121,-57.73 h -0.219 l -9.687,57.73 h -20.461 l 16.687,-75.18"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="44.708492"
height="44.678757"
viewBox="0 0 44.708492 44.678757"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-19.538916,-6.0484697)">
<path
id="path1"
d="M 477.988,50.2813 150.078,378.301"
style="fill:none;stroke:#ed1c24;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 150.309,50.2813 478.32,378.301"
style="fill:none;stroke:#ed1c24;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1009 B

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="60.768246"
height="47.311161"
viewBox="0 0 60.768246 47.311161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-11.286613,-4.5436252)">
<path
id="path1"
d="M 536.262,43.2891 H 88.8594 L 307.199,384.012 Z"
style="fill:none;stroke:#f47216;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 729 B

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="79.704201"
height="13.837466"
viewBox="0 0 79.704201 13.837466"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-1.9640666,-21.598534)">
<path
id="path1"
d="M 14.7305,229.371 H 612.512"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 14.7305,195.148 H 612.512"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 360.781,161.43 V 265.211"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 334.531,161.43 V 265.211"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="M 308.281,161.43 V 265.211"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="77.661423"
height="13.514667"
viewBox="0 0 77.661423 13.514667"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-2.8651066,-21.630801)">
<path
id="path1"
d="M 21.4883,214.289 H 603.949"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 317.34,163.609 v 101.36"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 996 B

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="80.001503"
height="13.889067"
viewBox="0 0 80.001503 13.889067"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-1.72396,-21.573601)">
<path
id="path1"
d="M 12.9297,213.32 H 612.941"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 330.57,161.23 V 265.398"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 295.301,161.23 V 265.398"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="62.261425"
height="36.760426"
viewBox="0 0 62.261425 36.760426"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-10.742706,-10.137068)">
<path
id="path1"
d="M 542.531,80.4688 H 85.5703 V 346.172 H 542.531 Z"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 171.309,194.469 V 319.781"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 203.34,144.422 c 0,-17.774 -14.078,-32.16 -31.449,-32.16 -17.36,0 -31.442,14.386 -31.442,32.16 0,17.75 14.082,32.148 31.442,32.148 17.371,0 31.449,-14.398 31.449,-32.148"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="79.574463"
height="16.0308"
viewBox="0 0 79.574463 16.0308"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-1.9640666,-20.502668)">
<path
id="path1"
d="M 611.539,204.898 H 14.7305"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 205.199,153.199 V 256.602"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 421.059,153.199 V 256.602"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 252.641,226.5 h 27.371 v 7.27 h -18.114 v 12.96 h 16.461 V 254 h -16.461 v 11.371 h 17.981 v 7.27 H 252.641 V 226.5"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="m 288.609,226.5 h 9.25 v 46.141 h -9.25 V 226.5"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="m 305.398,227.23 c 3.442,-1 8.063,-1.531 12.102,-1.531 7.66,0 17.648,2.781 17.648,13.75 0,5.621 -2.91,10.192 -8.796,10.981 v 0.129 c 4.699,1.793 8.597,4.761 8.597,11.043 0,9.847 -9.519,11.828 -15.211,11.828 -4.758,0 -9.187,-0.86 -12.886,-2.309 l 0.527,-7.402 c 3.242,1.582 6.742,2.441 10.383,2.441 4.16,0 7.929,-1.449 7.929,-6.082 0,-4.027 -4.031,-6.078 -10.242,-6.078 h -4.23 v -7.27 h 4.492 c 5.879,0 10.18,-1.789 10.18,-7.21 0,-4.692 -5.879,-6.54 -9.719,-6.54 -3.633,0 -7.07,0.86 -10.242,2.11 l -0.532,-7.86"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="m 357.488,232.578 c -6.808,0 -6.738,11.043 -6.738,16.863 0,5.95 0,17.118 6.738,17.118 6.883,0 6.883,-11.168 6.883,-17.118 0,-5.82 0.059,-16.863 -6.883,-16.863 z m 0,-6.879 c 13.352,0 16.133,13.031 16.133,23.742 0,10.707 -2.641,23.989 -16.133,23.989 -12.488,0 -16,-12.418 -16,-23.989 0,-9.652 1.793,-23.742 16,-23.742"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="62.267014"
height="32.830853"
viewBox="0 0 62.267014 32.830852"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-10.730922,-11.939098)">
<path
id="path1"
d="M 84.0391,334.121 142.23,275.219 h 342.809 l 58.91,58.902"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 84.0391,94.9609 142.23,153.871 h 342.809 l 58.91,-58.9101"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="50.141731"
height="49.562439"
viewBox="0 0 50.141731 49.562439"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-16.543733,-3.5736013)">
<path
id="path1"
d="m 495.141,214.539 c 0,-99.898 -81.942,-180.8593 -183.039,-180.8593 -101.082,0 -183.024,80.9613 -183.024,180.8593 0,99.891 81.942,180.859 183.024,180.859 101.097,0 183.039,-80.968 183.039,-180.859 z"
style="fill:none;stroke:#52afe6;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 282.57,188.359 c -7.14,-2.8 -15.82,-5.039 -24.359,-5.039 -19.039,0 -32.621,11.621 -32.621,30.801 0,18.199 12.039,31.641 30.519,31.641 8.961,0 17.082,-2.242 25.481,-6.864 l 2.101,21.141 c -9.382,2.801 -19.043,5.039 -28.98,5.039 -32.199,0 -56,-16.937 -56,-50.957 0,-36.961 30.098,-50.121 56.141,-50.121 13.296,0 21.558,2.102 28.976,3.922 l -1.258,20.437"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 309.449,165.68 h 33.742 l 12.879,75.039 h 0.282 l 13.859,-75.039 h 33.461 l 21.84,97.718 h -25.34 L 387.43,188.359 h -0.282 L 373.43,263.398 H 340.391 L 327.23,188.359 h -0.281 l -12.597,75.039 H 287.75 l 21.699,-97.718"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="60.768242"
height="47.310619"
viewBox="0 0 60.768242 47.31062"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-11.286615,-4.6865685)">
<path
id="path1"
d="M 536.262,44.6211 H 88.8594 L 307.199,385.34 Z"
style="fill:none;stroke:#f47216;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path2"
d="m 337.129,138.801 c -5.488,-2.153 -12.168,-3.879 -18.738,-3.879 -14.653,0 -25.102,8.937 -25.102,23.699 0,14 9.262,24.34 23.481,24.34 6.902,0 13.14,-1.723 19.601,-5.281 l 1.617,16.269 c -7.218,2.153 -14.648,3.879 -22.289,3.879 -24.777,0 -43.09,-13.039 -43.09,-39.207 0,-28.43 23.161,-38.562 43.192,-38.562 10.23,0 16.59,1.621 22.301,3.019 l -0.973,15.723"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="71.71196"
height="42.906773"
viewBox="0 0 71.71196 42.906773"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-5.9317732,-6.8933346)">
<path
id="path1"
d="M 577.328,58.6992 V 325.18 L 514.719,370.5 H 49.4883 V 102.531 L 110.109,58.6992 Z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 84.1289,246.891 h 20.2691 c 18.993,0 32.954,7.257 32.954,28.078 0,21.543 -13.563,27.609 -32.954,27.609 H 84.1289 Z m 14.8399,44.679 h 5.7502 c 10.851,0 17.312,-5.75 17.312,-16.84 0,-11.089 -6.222,-16.832 -17.312,-16.832 h -5.7502 v 33.672"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 186.73,263.48 v 2.641 c 0,12.359 -5.582,23.289 -20.98,23.289 -12.922,0 -22.262,-7.98 -22.262,-21.699 0,-13.723 10.453,-21.781 24.184,-21.781 5.34,0 10.609,0.8 15.226,2.629 v 10.14 c -4.296,-2.32 -8.769,-3.187 -12.679,-3.187 -7.02,0 -11.41,2.226 -12.207,7.968 z m -28.878,8.141 c 0.32,4.789 2.796,8.699 7.976,8.699 5.75,0 7.981,-3.91 7.981,-8.699 h -15.957"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 227.82,287.102 c -3.269,1.589 -7.101,2.308 -11.41,2.308 -13.719,0 -24.172,-7.98 -24.172,-21.781 0,-13.719 10.453,-21.699 24.172,-21.699 3.828,0 8.141,0.64 11.731,1.84 l -0.321,10.691 c -2.39,-1.281 -5.66,-2 -8.929,-2 -6.231,0 -11.813,4.309 -11.813,10.93 0,7.179 4.793,11.488 11.012,11.488 3.43,0 6.058,-0.719 8.691,-2.309 l 1.039,10.532"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="m 234.121,246.891 h 14.367 v 20.578 h 0.153 l 11.488,-20.578 h 17.402 l -15.64,22.5 14.437,19.07 H 260.609 L 248.641,270.98 h -0.153 v 35.75 h -14.367 v -59.839"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="m 324.121,263.48 v 2.641 c 0,12.359 -5.59,23.289 -20.98,23.289 -12.93,0 -22.262,-7.98 -22.262,-21.699 0,-13.723 10.449,-21.781 24.172,-21.781 5.347,0 10.609,0.8 15.238,2.629 v 10.14 c -4.309,-2.32 -8.777,-3.187 -12.687,-3.187 -7.024,0 -11.411,2.226 -12.204,7.968 z m -28.883,8.141 c 0.321,4.789 2.793,8.699 7.981,8.699 5.742,0 7.972,-3.91 7.972,-8.699 h -15.953"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="m 124.66,167.738 v 2.641 c 0,12.359 -5.582,23.293 -20.98,23.293 -12.9183,0 -22.2581,-7.973 -22.2581,-21.703 0,-13.719 10.4492,-21.778 24.1801,-21.778 5.339,0 10.609,0.797 15.226,2.637 v 10.133 c -4.297,-2.32 -8.769,-3.191 -12.68,-3.191 -7.019,0 -11.4097,2.23 -12.2066,7.968 z m -28.8787,8.141 c 0.3203,4.793 2.789,8.699 7.9807,8.699 5.738,0 7.976,-3.906 7.976,-8.699 H 95.7813"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path8"
d="m 133.84,151.148 h 14.359 v 41.571 H 133.84 Z m 14.359,58.883 H 133.84 V 199.5 h 14.359 v 10.531"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path9"
d="m 158.809,151.148 h 14.363 v 17.551 c 0,8.942 3.027,13.012 8.937,13.012 3.911,0 6.621,-2.641 6.621,-10.449 v -20.114 h 14.36 v 25.93 c 0,8.621 -4.551,16.594 -15.879,16.594 -6.539,0 -12.281,-2.633 -15.16,-8.613 h -0.16 v 7.66 h -13.082 v -41.571"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path10"
d="m 256.059,192.719 h -13 v -7.5 h -0.161 c -3.027,5.101 -7.976,8.453 -14.117,8.453 -12.929,0 -18.75,-9.25 -18.75,-21.621 0,-12.281 7.098,-20.903 18.508,-20.903 5.742,0 10.531,2.231 13.961,7.102 h 0.16 v -2.23 c 0,-8.141 -4.469,-12.051 -12.762,-12.051 -5.988,0 -9.66,1.281 -14.046,3.429 l -0.711,-11.328 c 3.347,-1.199 9.007,-2.629 15.871,-2.629 16.758,0 25.047,5.5 25.047,22.579 z m -31.188,-20.43 c 0,5.672 2.789,10.371 8.141,10.371 6.457,0 9.168,-5.18 9.168,-9.808 0,-6.383 -4.071,-10.692 -9.168,-10.692 -4.313,0 -8.141,3.668 -8.141,10.129"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path11"
d="m 306.57,167.738 v 2.641 c 0,12.359 -5.59,23.293 -20.992,23.293 -12.918,0 -22.258,-7.973 -22.258,-21.703 0,-13.719 10.461,-21.778 24.18,-21.778 5.34,0 10.609,0.797 15.238,2.637 v 10.133 c -4.308,-2.32 -8.777,-3.191 -12.687,-3.191 -7.02,0 -11.41,2.23 -12.199,7.968 z m -28.879,8.141 c 0.309,4.793 2.789,8.699 7.969,8.699 5.75,0 7.981,-3.906 7.981,-8.699 h -15.95"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path12"
d="m 312.469,152.191 c 4.23,-1.363 8.621,-2 15.082,-2 9.41,0 19.39,2.797 19.39,13.879 0,15.801 -21.39,10.7 -21.39,17.16 0,2.309 3.519,2.872 6.629,2.872 3.59,0 8.129,-0.961 11.172,-2.793 l 1.109,9.972 c -4.621,1.84 -9.652,2.391 -14.602,2.391 -8.289,0 -17.711,-3.91 -17.711,-13.481 0,-15 20.43,-10.05 20.43,-17.07 0,-2.871 -2.879,-3.351 -5.828,-3.351 -5.18,0 -10.129,1.429 -13.48,2.871 l -0.801,-10.45"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path13"
d="m 350.289,182.66 h 8.211 v -16.91 c 0,-9.891 3.121,-15.559 15.961,-15.559 3.59,0 6.301,0.481 9.019,0.797 l -0.48,9.34 c -1.441,-0.238 -2.949,-0.558 -4.391,-0.558 -4.55,0 -5.738,2.55 -5.738,7.582 v 15.308 h 10.051 v 10.059 h -10.051 v 13.722 l -14.133,-4.39 v -9.332 h -8.449 V 182.66"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path14"
d="M 433.66,192.719 H 419.301 V 175.16 c 0,-8.93 -3.031,-13 -8.942,-13 -3.91,0 -6.621,2.629 -6.621,10.449 v 20.11 h -14.359 v -25.93 c 0,-8.617 4.551,-16.598 15.883,-16.598 6.539,0 12.277,2.637 15.16,8.618 h 0.156 v -7.661 h 13.082 z m -18.75,5.101 h 11.488 v 11.489 H 414.91 Z m -18.191,0 h 11.492 v 11.489 H 396.719 V 197.82"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path15"
d="m 472.988,193.199 c -1.429,0.231 -3.187,0.473 -4.859,0.473 -6.231,0 -9.82,-3.352 -12.289,-8.613 h -0.16 v 7.66 h -13.078 v -41.571 h 14.359 v 17.551 c 0,8.141 3.75,13.012 10.449,13.012 1.668,0 3.27,0 4.86,-0.481 l 0.718,11.969"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path16"
d="m 475.621,151.148 h 35.75 v 10.532 h -19.312 l 18.992,19.55 v 11.489 H 476.02 v -10.528 h 18.589 l -18.988,-19.55 v -11.493"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path17"
d="m 514.238,182.66 h 8.223 v -16.91 c 0,-9.891 3.109,-15.559 15.949,-15.559 3.59,0 6.309,0.481 9.02,0.797 l -0.481,9.34 c -1.437,-0.238 -2.949,-0.558 -4.39,-0.558 -4.547,0 -5.739,2.55 -5.739,7.582 v 15.308 h 10.051 v 10.059 H 536.82 v 13.722 l -14.121,-4.39 v -9.332 h -8.461 V 182.66"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="77.166702"
height="31.680225"
viewBox="0 0 77.166703 31.680225"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-3.0906266,-12.348001)">
<path
id="path1"
d="m 266.809,262.781 h -69.297 v 69.309 h 69.297 z"
style="fill:none;stroke:#52afe6;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 23.1797,238.48 H 555.031"
style="fill:none;stroke:#52afe6;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 525.48,192.941 76.45,45.528 -76.45,45.543 v -91.071"
style="fill:#52afe6;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 136.078,118.75 h -14.406 v -7.422 h 14.406 V 96.9883 h 7.422 v 14.3397 h 14.422 v 7.422 H 143.5 v 14.34 h -7.422 v -14.34"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="m 197.23,96.9883 v 7.8517 h -20.332 c 8.133,7.988 18.762,17.691 18.762,28.25 0,9.781 -6.129,14.558 -15.621,14.558 -5.07,0 -10.277,-1.289 -14.769,-3.57 l 0.781,-8.277 c 3.277,2.14 7.277,4 11.488,4 4.07,0 8.133,-2.141 8.133,-6.711 0,-9.129 -16.91,-23.262 -21.551,-27.75 v -8.3517 h 33.109"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="m 220.051,311.16 4.988,-6.488 8.422,6.848 v -38.461 h 9.988 v 49.8 h -8.847 L 220.051,311.16"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="73.193268"
height="47.721371"
viewBox="0 0 73.193268 47.721371"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-5.0359332,-4.757068)">
<path
id="path1"
d="m 110.031,289.859 v 33.379 l 17.449,19.141 h 385.071"
style="fill:none;stroke:#52afe6;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path2"
d="m 497.469,290.852 89.25,51.527 -89.25,51.543 v -103.07"
style="fill:#52afe6;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path3"
d="M 348.309,87.5391 H 125.98 L 110.031,104.48 v 32.391"
style="fill:none;stroke:#52afe6;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path4"
d="M 333.23,139.07 422.48,87.5391 333.23,36.0117 V 139.07"
style="fill:#52afe6;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path5"
d="m 179.801,213.609 c 0,-38.531 -31.242,-69.757 -69.77,-69.757 -38.5193,0 -69.7615,31.226 -69.7615,69.757 0,38.532 31.2422,69.77 69.7615,69.77 38.528,0 69.77,-31.238 69.77,-69.77 z"
style="fill:none;stroke:#52afe6;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path6"
d="m 110.031,208.16 v 3.75 m 0,-3.75 v 3.75"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path7"
d="m 141.891,213.621 c 0,17.578 -14.27,31.848 -31.86,31.848 -17.5818,0 -31.8513,-14.27 -31.8513,-31.848 0,-17.59 14.2695,-31.859 31.8513,-31.859 17.59,0 31.86,14.269 31.86,31.859"
style="fill:#52afe6;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="77.053093"
height="43.783798"
viewBox="0 0 77.053093 43.783798"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-3.2786399,-6.5602679)">
<path
id="path1"
d="M 341.461,103.801 H 27.0898 V 377.898 H 341.461 Z"
style="fill:none;stroke:#231f20;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path2"
d="M 341.461,103.801 H 599.988 V 272.039 H 341.461"
style="fill:none;stroke:#231f20;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path3"
d="m 98.6211,103.801 v 56.59 h 65.5779 v -56.59"
style="fill:none;stroke:#231f20;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path4"
d="M 179.641,67.4492 V 121.391"
style="fill:none;stroke:#231f20;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path5"
d="M 229.781,67.4492 V 121.391"
style="fill:none;stroke:#231f20;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path6"
d="m 309.621,159.172 h 56.508"
style="fill:none;stroke:#231f20;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path7"
d="m 309.621,224.281 h 56.508"
style="fill:none;stroke:#231f20;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path8"
d="M 224.711,52.5586 203.762,88.8281 182.809,52.5586 h 41.902"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path9"
d="m 363.012,170.93 h 4.769 v 3.808 h 0.098 c 1.332,-2.008 4.402,-4.449 9.223,-4.449 8.16,0 11.597,7.199 11.597,14.621 0,7.262 -3.808,13.668 -11.648,13.668 -4.723,0 -7.422,-1.848 -8.961,-4.34 h -0.102 v 16.422 h -4.976 z m 20.398,13.609 c 0.102,-4.238 -2.121,-10.219 -7.469,-10.219 -5.621,0 -7.953,5.559 -7.953,10.059 0,5.043 2.59,10.172 8,10.172 5.403,0 7.524,-5.129 7.422,-10.012"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path10"
d="m 138.199,73.5313 c 2.012,1.6992 5.031,2.7578 7.84,2.7578 4.981,0 6.672,-2.3907 6.672,-7.0508 -1.961,0.1133 -3.332,0.1133 -5.289,0.1133 -5.141,0 -12.563,-2.1211 -12.563,-8.9024 0,-5.8789 4.082,-8.4297 9.911,-8.4297 4.55,0 7.199,2.4922 8.371,4.1289 h 0.097 v -3.4882 h 4.672 c -0.109,0.789 -0.219,2.2187 -0.219,5.3007 v 11.168 c 0,7.3711 -3.121,11.1797 -10.863,11.1797 -3.437,0 -6.406,-1.0586 -8.898,-2.5391 z m 14.512,-10.3829 c 0,-4.0273 -2.59,-7.0976 -7.52,-7.0976 -2.281,0 -5.031,1.539 -5.031,4.3984 0,4.7617 6.668,5.1914 9.371,5.1914 1.059,0 2.117,-0.1093 3.18,-0.1093 v -2.3829"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path11"
d="m 119.77,132.211 c 2.64,0.687 5.351,0.898 8.05,0.898 3.918,0 8.211,-1.269 8.211,-5.398 0,-4.723 -3.871,-5.93 -7.472,-5.93 -3.129,0 -6.2,0.629 -9.008,2.008 l -0.16,-8 c 3.019,-0.738 6.257,-1.328 9.91,-1.328 8.269,0 16.269,3.238 16.269,13.57 0,8.16 -6.629,11.438 -14.148,11.438 -0.633,0 -1.543,-0.11 -2.91,-0.207 v 6.136 h 15.629 v 6.68 h -24.11 l -0.261,-19.867"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="26.387999"
height="50.766693"
viewBox="0 0 26.387999 50.766693"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-28.350533,-3.234668)">
<path
id="path1"
d="m 363.16,101.879 c 0,-39.9298 -32.57,-72.2892 -72.762,-72.2892 -40.179,0 -72.769,32.3594 -72.769,72.2892 0,39.922 32.59,72.269 72.769,72.269 40.192,0 72.762,-32.347 72.762,-72.269 z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path2"
d="M 285.43,174.148 V 400.34 h 125.109"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path3"
d="M 285.43,268.828 H 410.539"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path4"
d="M 285.43,332.539 H 410.539"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="60.768246"
height="47.311066"
viewBox="0 0 60.768246 47.311066"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-11.286613,-4.6197603)">
<path
id="path1"
d="M 335.82,267.02 292.738,193 331.781,203.359 286.738,124.34"
style="fill:none;stroke:#f47216;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 265.289,146.539 0.32,-59.2577 51.18,29.8987 -51.5,29.359"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 536.262,42.7188 H 88.8594 L 307.199,383.441 Z"
style="fill:none;stroke:#f47216;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="77.012039"
height="29.204132"
viewBox="0 0 77.01204 29.204132"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-3.1572933,-13.937068)">
<path
id="path1"
d="M 596.27,108.641 H 28.6797 V 317.672 H 596.27 Z"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 96.5195,149.238 312.922,266.809 254.328,152.93 427.77,229.199"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 420.629,190.91 517.578,268.691 394.73,249.82 420.629,190.91"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="25.9984"
height="49.213737"
viewBox="0 0 25.9984 49.213737"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-28.828133,-3.7103276)">
<path
id="path1"
d="M 406.199,35.2695 H 221.211 V 229.93 h 184.988 z"
style="fill:none;stroke:#100f0d;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 406.199,35.2695 221.211,229.93 V 35.2695 h 184.988"
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 292.09,244.801 V 360.828"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 335.32,244.801 V 363.578"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="m 271.539,334.82 43.07,56.219 41.262,-56.219"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="74.721397"
height="34.585373"
viewBox="0 0 74.721398 34.585373"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-4.3973999,-11.317334)">
<path
id="path1"
d="M 37.9805,180.621 H 388.449 v -34.34 l 114.512,66.36 -114.512,66.339 V 244.629 H 37.9805 v -64.008"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 37.9805,180.621 H 388.449 v -34.34 l 114.512,66.36 -114.512,66.339 V 244.629 H 37.9805 Z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 517.371,337.32 h 71.02 V 87.9297 h -71.02 V 337.32"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 517.371,337.32 h 71.02 V 87.9297 h -71.02 z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="60.768242"
height="47.310593"
viewBox="0 0 60.768242 47.310593"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-11.286615,-4.578569)">
<path
id="path1"
d="M 536.262,43.0313 H 88.8594 L 307.199,383.75 Z"
style="fill:none;stroke:#f47216;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 250.461,117.91 h 54.18 v 14.211 H 270.5 v 17.02 h 31.121 v 14.218 H 270.5 v 15.512 h 32.949 v 14.219 h -52.988 v -75.18"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 310.781,117.91 h 21.86 l 9.269,17.449 9.371,-17.449 h 23.367 l -18.738,28.75 17.551,27.36 H 352.25 l -8.73,-16.801 -8.29,16.801 H 312.289 L 329.84,146.449 310.781,117.91"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="63.626572"
height="37.522346"
viewBox="0 0 63.626572 37.522346"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-9.9572931,-9.7788012)">
<path
id="path1"
d="M 546.879,77.4414 H 79.6797 V 348.859 H 546.879 Z"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 301.309,210.762 c 0,-47.992 -38.899,-86.891 -86.879,-86.891 -47.981,0 -86.879,38.899 -86.879,86.891 0,47.976 38.898,86.879 86.879,86.879 47.98,0 86.879,-38.903 86.879,-86.879 z"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 276.578,150.051 152.762,271.961"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 276.09,271.961 152.281,150.051"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="50.139999"
height="49.564117"
viewBox="0 0 50.139999 49.564117"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-16.524,-3.8240013)">
<path
id="path1"
d="m 494.98,212.648 c 0,-99.898 -81.941,-180.8589 -183.031,-180.8589 -101.078,0 -183.019,80.9609 -183.019,180.8589 0,99.891 81.941,180.872 183.019,180.872 101.09,0 183.031,-80.981 183.031,-180.872"
style="fill:#ed1c24;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 494.98,212.648 c 0,-99.898 -81.941,-180.8589 -183.031,-180.8589 -101.078,0 -183.019,80.9609 -183.019,180.8589 0,99.891 81.941,180.872 183.019,180.872 101.09,0 183.031,-80.981 183.031,-180.872 z"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 264.809,142.191 h 37.562 v 56.739 h 54.301 v 26.648 h -54.301 v 30.891 h 56.731 v 26.64 H 264.809 V 142.191"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="74.721397"
height="34.585373"
viewBox="0 0 74.721398 34.585373"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-4.2692666,-11.317334)">
<path
id="path1"
d="M 37.0195,180.621 H 387.48 v -34.34 L 502,212.641 387.48,278.98 V 244.629 H 37.0195 v -64.008"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 37.0195,180.621 H 387.48 v -34.34 L 502,212.641 387.48,278.98 V 244.629 H 37.0195 Z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 516.41,337.32 h 71.02 V 87.9297 H 516.41 V 337.32"
style="fill:#ed1c24;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 516.41,337.32 h 71.02 V 87.9297 h -71.02 z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="71.837395"
height="22.173725"
viewBox="0 0 71.837395 22.173725"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-5.9984399,-17.259945)">
<path
id="path1"
d="M 49.9883,178.66 H 444.77 v -38.551 l 129,74.5 -129,74.481 V 250.531 H 49.9883 V 178.66"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 49.9883,178.66 H 444.77 v -38.551 l 129,74.5 -129,74.481 V 250.531 H 49.9883 Z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="70.764389"
height="50.089214"
viewBox="0 0 70.764389 50.089214"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-6.1946132,-3.5346933)">
<path
id="path1"
d="M 93.6719,390.691 V 164.02 H 55.1211 L 129.621,35.0195 204.102,164.02 H 165.539 V 390.691 H 93.6719"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 93.6719,390.691 V 164.02 H 55.1211 L 129.621,35.0195 204.102,164.02 h -38.563 v 226.671 z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 529.98,35.0195 V 261.691 h 38.551 l -74.5,129 -74.48,-129 h 38.558 V 35.0195 h 71.871"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 529.98,35.0195 V 261.691 h 38.551 l -74.5,129 -74.48,-129 h 38.558 V 35.0195 Z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="50.141064"
height="49.562439"
viewBox="0 0 50.141064 49.562439"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-16.622933,-3.6069347)">
<path
id="path1"
d="m 495.73,214.289 c 0,-99.898 -81.949,-180.8593 -183.039,-180.8593 -101.082,0 -183.019,80.9613 -183.019,180.8593 0,99.891 81.937,180.859 183.019,180.859 101.09,0 183.039,-80.968 183.039,-180.859 z"
style="fill:none;stroke:#52afe6;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 208.539,165.43 h 26.039 v 39.34 h 37.66 v 18.48 h -37.66 v 21.422 h 39.344 v 18.476 H 208.539 V 165.43"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 300.801,165.43 h 33.738 l 12.883,75.039 h 0.277 l 13.86,-75.039 h 33.461 l 21.839,97.718 H 391.52 L 378.781,188.109 H 378.5 l -13.719,75.039 h -33.043 l -13.16,-75.039 h -0.277 l -12.602,75.039 h -26.597 l 21.699,-97.718"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="40.822491"
height="51.49313"
viewBox="0 0 40.822491 51.49313"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-21.210841,-3.0521347)">
<path
id="path1"
d="M 162.691,21.5703 306.941,172.07 204.871,151.23 393.219,334.301"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 427.219,270.012 465.25,404.309 329.93,370.121 427.219,270.012"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 346.859,154.5 h 11.61 v 19.781 h 0.121 L 372.129,154.5 h 14.793 l -17.352,23.281 16.102,20.27 H 371.699 L 358.59,180.27 h -0.121 v 17.781 h -11.61 V 154.5"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 405.32,154.5 h 11.61 v 43.551 h -10.231 l -14.789,-9.602 5.18,-7.871 8.23,5.871 V 154.5"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

28
public/signaturen/Gas.svg Normal file
View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="60.768242"
height="47.310577"
viewBox="0 0 60.768242 47.310578"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-11.286615,-4.7215026)">
<path
id="path1"
d="M 536.262,44.3594 H 88.8594 L 307.199,385.078 Z"
style="fill:none;stroke:#f47216;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path2"
d="m 348.691,165.672 h -31.132 v -14.223 h 11.742 V 136.59 c -1.832,-0.86 -4.41,-1.301 -7.11,-1.301 -14.64,0 -25.089,8.941 -25.089,23.699 0,17.563 13.347,24.34 29.617,24.34 4.742,0 12.062,-1.18 18.312,-4.848 l 1.61,16.379 c -7.321,2.801 -17.77,3.34 -22.719,3.34 -27.57,0 -47.5,-10.879 -47.5,-39.211 0,-28.429 23.156,-38.558 43.187,-38.558 6.891,0 16.371,0.218 29.082,3.449 v 41.793"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="74.721306"
height="34.585373"
viewBox="0 0 74.721306 34.585373"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-4.4880266,-11.317334)">
<path
id="path1"
d="M 38.6602,180.621 H 389.129 v -34.34 l 114.512,66.36 -114.512,66.339 V 244.629 H 38.6602 v -64.008"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 38.6602,180.621 H 389.129 v -34.34 l 114.512,66.36 -114.512,66.339 V 244.629 H 38.6602 Z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 518.051,337.32 H 589.07 V 87.9297 H 518.051 V 337.32"
style="fill:#f47216;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 518.051,337.32 H 589.07 V 87.9297 h -71.019 z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="71.837311"
height="22.173725"
viewBox="0 0 71.837311 22.173725"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-6.2171865,-17.259945)">
<path
id="path1"
d="M 51.6289,178.66 H 446.41 v -38.551 l 129,74.5 -129,74.481 V 250.531 H 51.6289 V 178.66"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 51.6289,178.66 H 446.41 v -38.551 l 129,74.5 -129,74.481 V 250.531 H 51.6289 Z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="70.764442"
height="50.089237"
viewBox="0 0 70.764443 50.089237"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-6.4133568,-3.5347)">
<path
id="path1"
d="M 95.3086,390.691 V 164.02 H 56.7617 L 131.262,35.0195 205.738,164.02 H 167.18 V 390.691 H 95.3086"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 95.3086,390.691 V 164.02 H 56.7617 L 131.262,35.0195 205.738,164.02 H 167.18 v 226.671 z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 531.621,35.0195 V 261.691 h 38.551 l -74.5,129 -74.481,-129 H 459.75 V 35.0195 h 71.871"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 531.621,35.0195 V 261.691 h 38.551 l -74.5,129 -74.481,-129 H 459.75 V 35.0195 Z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="50.141598"
height="49.564117"
viewBox="0 0 50.141598 49.564117"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-16.5292,-3.8240013)">
<path
id="path1"
d="m 495.031,212.648 c 0,-99.898 -81.941,-180.8589 -183.043,-180.8589 -101.078,0 -183.019,80.9609 -183.019,180.8589 0,99.891 81.941,180.872 183.019,180.872 101.102,0 183.043,-80.981 183.043,-180.872"
style="fill:#f47216;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 495.031,212.648 c 0,-99.898 -81.941,-180.8589 -183.043,-180.8589 -101.078,0 -183.019,80.9609 -183.019,180.8589 0,99.891 81.941,180.872 183.019,180.872 101.102,0 183.043,-80.981 183.043,-180.872 z"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 364.359,174.898 c -10.3,-4.039 -22.82,-7.269 -35.129,-7.269 -27.46,0 -47.039,16.762 -47.039,44.422 0,26.238 17.36,45.629 44.008,45.629 12.922,0 24.629,-3.231 36.75,-9.899 l 3.02,30.489 c -13.52,4.039 -27.449,7.269 -41.789,7.269 -46.43,0 -80.75,-24.43 -80.75,-73.488 0,-53.301 43.398,-72.281 80.949,-72.281 19.18,0 31.101,3.031 41.801,5.652 l -1.821,29.476"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="60.768242"
height="47.310619"
viewBox="0 0 60.768242 47.310619"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-11.286615,-4.4332352)">
<path
id="path1"
d="M 395.699,91.7109 229.41,170.781"
style="fill:none;stroke:#f47216;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 394.879,170.781 230.23,91.7109"
style="fill:none;stroke:#f47216;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 276.262,93.6602 h 25.957 l 9.902,57.7308 h 0.219 L 323,93.6602 h 25.738 l 16.813,75.1798 h -19.5 l -9.801,-57.731 h -0.211 L 325.48,168.84 h -25.421 l -10.118,-57.731 h -0.222 l -9.688,57.731 h -20.472 l 16.703,-75.1798"
style="fill:#f47216;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 536.262,44.1211 H 88.8594 L 307.199,384.84 Z"
style="fill:none;stroke:#f47216;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

38
public/signaturen/HRF.svg Normal file
View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="75.185463"
height="38.587479"
viewBox="0 0 75.185463 38.587479"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-4.2666666,-9.3162679)">
<path
id="path1"
d="M 590.891,72.9219 H 37 V 352.328 h 553.891 z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 429.82,352.328 430.391,72.9219"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="M 430.391,72.9219 583.891,210.801 429.828,348.129"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="M 78.3008,257.09 305.191,146.461 37,88.3281"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="72.734825"
height="45.84901"
viewBox="0 0 72.734825 45.84901"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-5.2291732,-5.6854679)">
<path
id="path1"
d="M 579.73,45.6914 H 44.2188 V 379.559 H 579.73 Z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 44.2188,211.871 H 579.73"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 308.109,296.09 c 0,0 67.411,48.969 99.629,48.539 32.242,-0.41 46.883,-19.238 46.473,-48.539 -0.41,-29.309 -10.891,-49.41 -38.922,-49.41 -28.059,0 -85.398,34.332 -107.18,49.41 z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 308.109,296.09 c 0,0 -59.859,-36.852 -74.089,-43.539 -14.239,-6.711 -74.098,-25.961 -74.098,38.918 0,64.89 54.008,54.84 75.777,43.543 21.762,-11.301 72.41,-38.922 72.41,-38.922 z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="71.837273"
height="22.173725"
viewBox="0 0 71.837273 22.173725"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-6.1265598,-17.259945)">
<path
id="path1"
d="M 50.9492,178.66 H 445.73 v -38.551 l 129,74.5 -129,74.481 V 250.531 H 50.9492 V 178.66"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="M 50.9492,178.66 H 445.73 v -38.551 l 129,74.5 -129,74.481 V 250.531 H 50.9492 Z"
style="fill:none;stroke:#1b1816;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="50.746876"
height="50.747902"
viewBox="0 0 50.746876 50.747902"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-16.319761,-3.236029)">
<path
id="path1"
d="M 312.699,397.93 127.398,212.629 312.699,27.3203 498,212.629 Z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 312.699,247.43 v 17.492"
style="fill:none;stroke:#00adef;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 312.699,161.5 v 73.871"
style="fill:none;stroke:#00adef;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="73.323975"
height="36.323971"
viewBox="0 0 73.323975 36.323971"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-4.5932266,-10.448001)">
<path
id="path1"
d="m 170.66,81.4102 c -72.4803,0 -131.2108,58.7378 -131.2108,131.2188 0,72.461 58.7305,131.211 131.2108,131.211 V 81.4102"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 301.879,212.629 c 0,-72.481 -58.75,-131.2188 -131.219,-131.2188 -72.4803,0 -131.2108,58.7378 -131.2108,131.2188 0,72.461 58.7305,131.211 131.2108,131.211 72.469,0 131.219,-58.75 131.219,-131.211 z"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 382.988,209.879 c 3.871,1 7.821,1.312 11.774,1.312 5.726,0 12,-1.851 12,-7.89 0,-6.903 -5.653,-8.68 -10.922,-8.68 -4.57,0 -9.059,0.93 -13.16,2.949 l -0.231,-11.699 c 4.41,-1.082 9.129,-1.93 14.481,-1.93 12.07,0 23.769,4.719 23.769,19.821 0,11.918 -9.679,16.718 -20.679,16.718 -0.93,0 -2.239,-0.152 -4.258,-0.3 v 8.98 h 22.847 v 9.75 h -35.23 l -0.391,-29.031"
style="fill:#0a1323;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 430.371,209.879 c 3.879,1 7.82,1.312 11.77,1.312 5.73,0 12,-1.851 12,-7.89 0,-6.903 -5.653,-8.68 -10.911,-8.68 -4.57,0 -9.058,0.93 -13.171,2.949 l -0.231,-11.699 c 4.422,-1.082 9.141,-1.93 14.481,-1.93 12.082,0 23.769,4.719 23.769,19.821 0,11.918 -9.68,16.718 -20.668,16.718 -0.93,0 -2.25,-0.152 -4.262,-0.3 v 8.98 h 22.84 v 9.75 h -35.226 l -0.391,-29.031"
style="fill:#0a1323;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="m 470.328,183.941 h 9.75 l 15.024,55.899 h -9.75 l -15.024,-55.899"
style="fill:#0a1323;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="m 501.379,184.871 h 15.48 l 19.36,42.348 v 11.691 h -38.867 v -10.219 h 24.16 l -20.133,-43.82"
style="fill:#0a1323;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="m 546.672,209.879 c 3.867,1 7.816,1.312 11.769,1.312 5.731,0 12,-1.851 12,-7.89 0,-6.903 -5.652,-8.68 -10.921,-8.68 -4.559,0 -9.051,0.93 -13.161,2.949 l -0.23,-11.699 c 4.41,-1.082 9.141,-1.93 14.48,-1.93 12.082,0 23.77,4.719 23.77,19.821 0,11.918 -9.68,16.718 -20.668,16.718 -0.93,0 -2.25,-0.152 -4.262,-0.3 v 8.98 h 22.84 v 9.75 h -35.23 l -0.387,-29.031"
style="fill:#0a1323;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="34.833794"
height="52.252541"
viewBox="0 0 34.833794 52.252541"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-24.347406,-2.4305347)">
<path
id="path1"
d="m 438.859,299.648 c 0,-58.929 -47.781,-106.718 -106.711,-106.718 -58.937,0 -106.718,47.789 -106.718,106.718 0,58.942 47.781,106.723 106.718,106.723 58.93,0 106.711,-47.781 106.711,-106.723 z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path2"
d="M 225.43,299.648 V 59.9102"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path3"
d="M 266.191,22.9883 186.422,103.852"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path4"
d="M 266.461,103.578 186.141,23.2617"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path5"
d="m 311.5,268.789 h 16.449 v 24.852 h 23.781 v 11.668 h -23.781 v 13.531 h 24.84 v 11.672 H 311.5 v -61.723"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="49.806267"
height="49.806744"
viewBox="0 0 49.806267 49.806744"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-16.892133,-3.753468)">
<path
id="path1"
d="M 495.238,32.8984 H 131.691 V 396.449 L 495.238,32.8984"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path2"
d="M 495.238,32.8984 H 131.691 V 396.449 h 363.547 z"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="22.575035"
height="51.323757"
viewBox="0 0 22.575035 51.323757"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-30.331749,-2.9947885)">
<path
id="path1"
d="M 232.488,27.2109 H 391.801 L 313.488,402.141 Z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 730 B

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="34.509331"
height="48.470772"
viewBox="0 0 34.509331 48.470772"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-24.621333,-4.4212013)">
<path
id="path1"
d="M 438.48,37.9102 H 189.66 V 391.441 h 248.82 z"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path2"
d="m 188.031,221.441 c 93.528,65.887 192.989,65.84 250.641,19.368"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path3"
d="m 188.031,182.621 c 90,-58.23 187.699,-62.351 250.641,-11.769"
style="fill:none;stroke:#00adef;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
<path
id="path4"
d="m 312.488,190.891 c 8.313,0 15.043,6.738 15.043,15.039 0,8.308 -6.73,15.039 -15.043,15.039 -8.297,0 -15.039,-6.731 -15.039,-15.039 0,-8.301 6.742,-15.039 15.039,-15.039"
style="fill:#00adef;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,57.28)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="78.285355"
height="17.689066"
viewBox="0 0 78.285355 17.689066"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-2.6786399,-19.348001)">
<path
id="path1"
d="M 20.0898,233.039 H 607.23"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 219.762,233.281 c 0,-24.203 -19.672,-43.82 -43.942,-43.82 -24.261,0 -43.929,19.617 -43.929,43.82 0,24.199 19.668,43.809 43.929,43.809 24.27,0 43.942,-19.61 43.942,-43.809"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 219.762,233.281 c 0,-24.203 -19.672,-43.82 -43.942,-43.82 -24.261,0 -43.929,19.617 -43.929,43.82 0,24.199 19.668,43.809 43.929,43.809 24.27,0 43.942,-19.61 43.942,-43.809 z"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path4"
d="m 495.43,233.281 c 0,-24.203 -19.668,-43.82 -43.942,-43.82 -24.258,0 -43.929,19.617 -43.929,43.82 0,24.199 19.671,43.809 43.929,43.809 24.274,0 43.942,-19.61 43.942,-43.809"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path5"
d="m 495.43,233.281 c 0,-24.203 -19.668,-43.82 -43.942,-43.82 -24.258,0 -43.929,19.617 -43.929,43.82 0,24.199 19.671,43.809 43.929,43.809 24.274,0 43.942,-19.61 43.942,-43.809 z"
style="fill:none;stroke:#231f20;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path6"
d="m 240.102,150.16 h 11.609 v 43.551 h -10.242 l -14.778,-9.602 5.168,-7.871 8.243,5.871 V 150.16"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path7"
d="m 293.879,192.469 c -3.301,1.312 -6.859,1.992 -11.289,1.992 -14.41,0 -19.219,-11.039 -19.219,-24.391 0,-10.922 4.309,-20.648 16.719,-20.648 9.609,0 16.351,6.668 16.351,14.719 0,7.8 -5.05,13.73 -13.043,13.73 -4.367,0 -7.359,-1.5 -9.418,-4.43 h -0.128 c 0.07,7.668 2.437,12.789 10.168,12.789 4.25,0 7.3,-1.058 9.179,-2.308 z m -19.527,-28.891 c 0,3.371 1.878,6.434 5.679,6.434 4.059,0 5.547,-3.313 5.547,-6.934 0,-3.367 -1.93,-6.18 -5.547,-6.18 -3.742,0 -5.679,3.122 -5.679,6.68"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path8"
d="m 321.578,150.16 h 11.231 v 16.102 h 0.132 l 8.981,-16.102 h 13.609 l -12.23,17.602 11.289,14.91 h -12.289 l -9.36,-13.66 h -0.132 v 27.949 H 321.578 V 150.16"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path9"
d="m 371.43,150.16 h 14.359 l 14.84,43.551 h -11.91 L 378.98,159.52 h -0.121 l -9.668,34.191 H 356.84 l 14.59,-43.551"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

Some files were not shown because too many files have changed in this diff Show More