82 Commits

Author SHA1 Message Date
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
241 changed files with 21057 additions and 4838 deletions

View File

@@ -1,7 +1,7 @@
node_modules node_modules
.next .next
.git .git
Signaturen /Signaturen/
*.tar *.tar
*.pdf *.pdf
*.py *.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 App
WEB_PORT=3000 WEB_PORT=3000
NODE_ENV=development 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) # Large binary files (do not commit)
*.tar *.tar
*.zip *.zip
*.mp4 # Allow MP4s in public folder (used by the app)
!public/*.mp4
!public/**/*.mp4
lageplan-web.tar lageplan-web.tar
# Reference materials (keep locally, not in git) # Reference materials (keep locally, not in git)
Signaturen/ /Signaturen/
Reglement_*/ 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 (contains secrets)
stack.env 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 # Stage 1: Dependencies
FROM node:20-alpine AS deps FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat openssl RUN apk add --no-cache libc6-compat openssl
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json* ./ 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 # Stage 2: Builder
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
@@ -20,42 +22,50 @@ RUN npx prisma generate
COPY .env.docker .env COPY .env.docker .env
# Build Next.js # Build Next.js
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build # .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 # Stage 3: Runner
FROM node:20-alpine AS runner FROM node:20-alpine AS runner
RUN apk add --no-cache openssl 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 WORKDIR /app
# Fast: only chown the /app directory itself, not recursively
ENV NODE_ENV production RUN chown nextjs:nodejs /app
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
USER nextjs 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 EXPOSE 3000
ENV PORT 3000 ENV PORT=3000
ENV HOSTNAME "0.0.0.0" ENV HOSTNAME="0.0.0.0"
CMD ["sh", "docker-entrypoint.sh"] 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 | - Gitea läuft mit Container Registry aktiviert
|-------|-------------| - Gitea Actions Runner ist registriert (`deploy/docker-compose.runner.yml`)
| `lageplan-web-v1.0.0.tar` | Docker Image (~92 MB) | - Portainer Stack ist deployed mit korrekten Environment-Variablen
| `portainer-stack.yml` | Stack YAML für Portainer |
| `.env.example` | Environment Variables Vorlage |
--- ---
## Schritt 1: Image auf Server laden ## Schritt 1: Gitea Container Registry aktivieren
```bash In Gitea:
# Kopieren 1. **Admin-Konsole****Konfiguration****Pakete**
scp lageplan-web-v1.0.0.tar user@server:/tmp/ 2. **Container Registry** auf `Aktiviert` setzen
3. Speichern
# Auf dem Server laden Oder direkt in der `app.ini`:
docker load -i /tmp/lageplan-web-v1.0.0.tar ```ini
[packages]
ENABLED = true
# Prüfen [package.container_registry]
docker images | grep lageplan 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` 1. **Portainer**`Stacks``+ Add stack`
2. **Name**: `lageplan` 2. **Name**: `lageplan`
3. **Web editor**: Inhalt von `portainer-stack.yml` einfügen 3. **Build method**: `Repository`
4. **Environment variables** setzen: 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 | ### Environment Variables setzen:
|----------|------|
| `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)* |
> **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 1. Gitea Repo → **Einstellungen****Webhooks****Neuer Webhook**`Gitea`
npx prisma db push 2. **Ziel-URL**: Die kopierte Portainer Webhook URL
npx prisma db seed 3. **HTTP-Methode**: `POST`
``` 4. **Trigger**: Nur `Push events` (oder auch `Branch filter: main`)
5. **Webhook aktivieren** → Hinzufügen
Oder per SSH:
```bash
docker exec -it lageplan-web-1 npx prisma db push
docker exec -it lageplan-web-1 npx prisma db seed
```
--- ---
## Schritt 4: Zugriff ## Schritt 6: Erstes Deployment testen
- **Web App**: `http://SERVER_IP:3000` 1. Lokal einen Push auf `main` machen:
- **Login**: `admin@lageplan.local` / `admin123` ```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 ```bash
# Lokal: neues Image bauen + exportieren # Auf dem Portainer-Host
docker compose build web docker pull git.purepixel.ch/adminpepe/lageplan:latest
docker tag lageplan-web:latest lageplan-web:v1.1.0 docker compose -f docker-compose.portainer.yml up -d web
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
``` ```
--- ---

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 WEB_PORT=3000
NEXTAUTH_URL=http://SERVER_IP:3000 NEXTAUTH_URL=http://SERVER_IP:3000
NEXTAUTH_SECRET=HIER_LANGEN_ZUFAELLIGEN_STRING_GENERIEREN 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: # Verwendung in Portainer:
# 1. Stacks → Add Stack → "Gitea" # 1. Stacks → Add Stack → "Gitea"
@@ -12,6 +12,10 @@
# 3. Repository "lageplan" erstellen # 3. Repository "lageplan" erstellen
# 4. Vom PC aus: git init → git remote add origin → git push # 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. # Daten werden in gitea_data persistiert.
############################################## ##############################################
@@ -27,6 +31,9 @@ services:
- GITEA__server__ROOT_URL=https://git.purepixel.ch - GITEA__server__ROOT_URL=https://git.purepixel.ch
- GITEA__server__HTTP_PORT=3000 - GITEA__server__HTTP_PORT=3000
- GITEA__server__LFS_START_SERVER=true - GITEA__server__LFS_START_SERVER=true
# Container Registry aktivieren
- GITEA__packages__ENABLED=true
- GITEA__package__container_registry__ENABLED=true
volumes: volumes:
- gitea_data:/data - gitea_data:/data
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
@@ -43,4 +50,3 @@ volumes:
networks: networks:
lageplan_lageplan-net: lageplan_lageplan-net:
external: true external: true

View File

@@ -1,21 +1,28 @@
############################################## ##############################################
# Lageplan — Portainer Stack Configuration # Lageplan — Portainer Stack (Watchtower Auto-Update)
# #
# Verwendung in Portainer: # Setup in Portainer:
# 1. Stacks → Add Stack # 1. Stacks → Add Stack → "Repository"
# 2. "Upload" oder diesen Inhalt einfügen # 2. Git-URL: https://git.purepixel.ch/adminpepe/Lageplan.git
# 3. Environment-Variablen setzen (siehe unten) # 3. Compose-Pfad: docker-compose.portainer.yml
# 4. Deploy # 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: # Benötigte Environment-Variablen:
# POSTGRES_USER (default: lageplan) # POSTGRES_USER (default: lageplan)
# POSTGRES_PASSWORD (ÄNDERN!) # POSTGRES_PASSWORD (ÄNDERN!)
# POSTGRES_DB (default: lageplan) # POSTGRES_DB (default: lageplan)
# NEXTAUTH_SECRET (ÄNDERN! — z.B. openssl rand -base64 32) # 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_USER (default: minioadmin)
# MINIO_ROOT_PASSWORD (ÄNDERN!) # MINIO_ROOT_PASSWORD (ÄNDERN!)
# MINIO_PUBLIC_URL (z.B. https://s3.example.com) # 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: services:
@@ -76,13 +83,14 @@ services:
- lageplan - lageplan
# ─── Lageplan Web App ────────────────────── # ─── Lageplan Web App ──────────────────────
# Image kommt aus Gitea Container Registry (gebaut via Gitea Actions)
web: web:
image: 192.168.1.183:3100/adminpepe/lageplan:latest image: git.purepixel.ch/adminpepe/lageplan:latest
restart: unless-stopped restart: unless-stopped
environment: environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-lageplan}:${POSTGRES_PASSWORD:-lageplan_secret}@db:5432/${POSTGRES_DB:-lageplan} DATABASE_URL: postgresql://${POSTGRES_USER:-lageplan}:${POSTGRES_PASSWORD:-lageplan_secret}@db:5432/${POSTGRES_DB:-lageplan}
NEXTAUTH_URL: ${NEXTAUTH_URL:-https://localhost:3000} 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_ENDPOINT: minio
MINIO_PORT: "9000" MINIO_PORT: "9000"
MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-minioadmin} MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-minioadmin}
@@ -99,6 +107,25 @@ services:
condition: service_healthy condition: service_healthy
networks: networks:
- lageplan - 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: volumes:
postgres_data: postgres_data:

View File

@@ -66,7 +66,7 @@ services:
environment: environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-lageplan}:${POSTGRES_PASSWORD:-lageplan_secret}@db:5432/${POSTGRES_DB:-lageplan} DATABASE_URL: postgresql://${POSTGRES_USER:-lageplan}:${POSTGRES_PASSWORD:-lageplan_secret}@db:5432/${POSTGRES_DB:-lageplan}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000} 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_ENDPOINT: minio
MINIO_PORT: 9000 MINIO_PORT: 9000
MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-minioadmin} 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) ─── # ─── Step 1: Run migrations (uses PrismaClient raw SQL, no CLI needed) ───
echo "[1/3] Running database migrations..." 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 ─── # ─── Step 2: Conditional seeding ───
echo "[2/3] Checking if seeding is needed..." echo "[2/3] Checking if seeding is needed..."
@@ -34,6 +38,44 @@ else
fi fi
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..." echo "[3/3] Starting server..."
exec node server-custom.js 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,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)

36
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "lageplan", "name": "lageplan",
"version": "1.0.1", "version": "1.3.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lageplan", "name": "lageplan",
"version": "1.0.1", "version": "1.3.2",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.1.0", "@dnd-kit/core": "^6.1.0",
@@ -53,7 +53,8 @@
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"zod": "^3.22.4" "zod": "^3.22.4",
"zustand": "^5.0.11"
}, },
"devDependencies": { "devDependencies": {
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
@@ -10566,6 +10567,35 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
},
"node_modules/zustand": {
"version": "5.0.11",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz",
"integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "lageplan", "name": "lageplan",
"version": "1.2.2", "version": "1.6.7",
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation", "description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -12,9 +12,15 @@
"db:push": "prisma db push", "db:push": "prisma db push",
"db:migrate": "prisma migrate dev", "db:migrate": "prisma migrate dev",
"db:migrate:prod": "prisma migrate deploy", "db:migrate:prod": "prisma migrate deploy",
"db:seed": "npx tsx prisma/seed.ts", "db:seed": "node prisma/seed.js",
"db:studio": "prisma studio", "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": { "dependencies": {
"@dnd-kit/core": "^6.1.0", "@dnd-kit/core": "^6.1.0",
@@ -61,7 +67,8 @@
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"zod": "^3.22.4" "zod": "^3.22.4",
"zustand": "^5.0.11"
}, },
"prisma": { "prisma": {
"seed": "node prisma/seed.js" "seed": "node prisma/seed.js"

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. * Database migration script using PrismaClient raw SQL.
* Does NOT require the Prisma CLI (npx prisma) — only the runtime client. * Does NOT require the Prisma CLI (npx prisma) — only the runtime client.
* Safe to run multiple times (all statements are idempotent). * 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') const { PrismaClient } = require('@prisma/client')
@@ -53,6 +59,7 @@ async function migrate() {
// Users // Users
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "lastLoginAt" TIMESTAMP`, `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 "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 "emailVerificationToken" TEXT`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "resetToken" TEXT`, `ALTER TABLE users ADD COLUMN IF NOT EXISTS "resetToken" TEXT`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "resetTokenExpiry" TIMESTAMP`, `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 "planBounds" JSONB`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "einsatzleiter" TEXT`, `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 "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 "editingById" TEXT`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingUserName" TEXT`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingUserName" TEXT`,
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingSessionId" TEXT`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingSessionId" TEXT`,
@@ -72,6 +82,10 @@ async function migrate() {
// Journal // Journal
`ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS "isCorrected" BOOLEAN DEFAULT false`, `ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS "isCorrected" BOOLEAN DEFAULT false`,
`ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS "correctionOfId" TEXT`, `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`,
] ]
let added = 0 let added = 0
for (const sql of columnMigrations) { for (const sql of columnMigrations) {
@@ -110,6 +124,28 @@ async function migrate() {
"createdById" TEXT REFERENCES users(id) ON DELETE SET NULL, "createdById" TEXT REFERENCES users(id) ON DELETE SET NULL,
UNIQUE("tenantId", "reportNumber") 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")`,
] ]
for (const sql of tableMigrations) { for (const sql of tableMigrations) {
try { await prisma.$executeRawUnsafe(sql) } catch (e) { /* table might already exist */ } try { await prisma.$executeRawUnsafe(sql) } catch (e) { /* table might already exist */ }
@@ -121,10 +157,9 @@ async function migrate() {
await prisma.$executeRawUnsafe(`UPDATE tenants SET "subscriptionStatus" = 'ACTIVE' WHERE "subscriptionStatus" = 'TRIAL'`) await prisma.$executeRawUnsafe(`UPDATE tenants SET "subscriptionStatus" = 'ACTIVE' WHERE "subscriptionStatus" = 'TRIAL'`)
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
// ─── Step 6: Clean up orphan users ─── // ─── Step 6: Detect orphan users (log only, no deletion) ───
console.log(' [6/7] Cleaning up orphan users...') console.log(' [6/7] Checking for orphan users...')
try { try {
// Find users who are NOT SERVER_ADMIN and have NO tenant membership
const orphans = await prisma.user.findMany({ const orphans = await prisma.user.findMany({
where: { where: {
role: { not: 'SERVER_ADMIN' }, role: { not: 'SERVER_ADMIN' },
@@ -133,22 +168,15 @@ async function migrate() {
select: { id: true, email: true, name: true }, select: { id: true, email: true, name: true },
}) })
if (orphans.length > 0) { 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) { for (const o of orphans) {
console.log(` - ${o.email} (${o.name})`) 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 { } else {
console.log(' No orphan users found') console.log(' No orphan users found')
} }
} catch (e) { } catch (e) {
console.log(' Orphan cleanup skipped:', e.message) console.log(' Orphan check skipped:', e.message)
} }
// ─── Step 7: Backfill logoFileKey from logoUrl ─── // ─── Step 7: Backfill logoFileKey from logoUrl ───
@@ -214,7 +242,7 @@ async function migrate() {
console.log(' Privacy consent columns skipped:', e.message) console.log(' Privacy consent columns skipped:', e.message)
} }
// ─── Step 12: Create tenant_symbols table ─── // ─── Step 12: Create tenant_symbols table (CRITICAL — fail-fast) ───
console.log(' [12] Creating tenant_symbols table...') console.log(' [12] Creating tenant_symbols table...')
try { try {
await prisma.$executeRawUnsafe(` await prisma.$executeRawUnsafe(`
@@ -222,13 +250,200 @@ async function migrate() {
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(), id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
"isActive" BOOLEAN NOT NULL DEFAULT true, "isActive" BOOLEAN NOT NULL DEFAULT true,
"tenantId" TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, "tenantId" TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
"iconId" TEXT NOT NULL REFERENCES icon_assets(id) ON DELETE CASCADE, "iconId" TEXT REFERENCES icon_assets(id) ON DELETE SET NULL,
UNIQUE("tenantId", "iconId") UNIQUE("tenantId", "iconId")
) )
`) `)
console.log(' tenant_symbols table created (or already exists)') console.log(' tenant_symbols table created (or already exists)')
} catch (e) { } catch (e) {
console.log(' tenant_symbols table skipped:', e.message) 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') 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? notes String?
hiddenIconIds String[] @default([]) hiddenIconIds String[] @default([])
journalSuggestions String[] @default([]) journalSuggestions String[] @default([])
// Modul-Baukasten: Cockpit-Konfiguration (siehe src/lib/modules.ts). NULL = Standard.
modulesConfig Json?
// Privacy consent // Privacy consent
privacyAccepted Boolean @default(false) privacyAccepted Boolean @default(false)
@@ -90,6 +92,7 @@ model Tenant {
iconCategories IconCategory[] iconCategories IconCategory[]
iconAssets IconAsset[] iconAssets IconAsset[]
tenantSymbols TenantSymbol[] tenantSymbols TenantSymbol[]
tenantCategories TenantCategory[]
upgradeRequests UpgradeRequest[] upgradeRequests UpgradeRequest[]
dictionaryEntries DictionaryEntry[] dictionaryEntries DictionaryEntry[]
rapports Rapport[] rapports Rapport[]
@@ -117,6 +120,8 @@ model User {
name String name String
role Role @default(OPERATOR) role Role @default(OPERATOR)
emailVerified Boolean @default(true) emailVerified Boolean @default(true)
// Super-Admin kann einzelne Benutzer sperren; gesperrte können sich nicht mehr anmelden.
isActive Boolean @default(true)
emailVerificationToken String? @unique emailVerificationToken String? @unique
resetToken String? @unique resetToken String? @unique
resetTokenExpiry DateTime? resetTokenExpiry DateTime?
@@ -150,6 +155,11 @@ model TenantMembership {
model Project { model Project {
id String @id @default(uuid()) id String @id @default(uuid())
einsatzNr String? 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 title String
location String? location String?
description String? description String?
@@ -157,7 +167,13 @@ model Project {
journalfuehrer String? journalfuehrer String?
mapCenter Json @default("{\"lng\": 8.5417, \"lat\": 47.3769}") mapCenter Json @default("{\"lng\": 8.5417, \"lat\": 47.3769}")
mapZoom Float @default(15) mapZoom Float @default(15)
// Windrichtung in Grad (meteorologisch „Wind aus"), fürs Lagebild. NULL = nicht gesetzt.
windDirection Int?
isLocked Boolean @default(false) 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)
// Live editing lock (session-based for same-account multi-device) // Live editing lock (session-based for same-account multi-device)
editingById String? editingById String?
@@ -179,11 +195,32 @@ model Project {
journalEntries JournalEntry[] journalEntries JournalEntry[]
journalCheckItems JournalCheckItem[] journalCheckItems JournalCheckItem[]
journalPendenzen JournalPendenz[] journalPendenzen JournalPendenz[]
exerciseGoals ExerciseGoal[]
rapports Rapport[] rapports Rapport[]
moduleItems ModuleItem[]
@@map("projects") @@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 { model Feature {
id String @id @default(uuid()) id String @id @default(uuid())
type String type String
@@ -317,6 +354,24 @@ model JournalPendenz {
@@map("journal_pendenzen") @@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 { model JournalCheckTemplate {
id String @id @default(uuid()) id String @id @default(uuid())
label String label String
@@ -378,22 +433,67 @@ model UpgradeRequest {
@@map("upgrade_requests") @@map("upgrade_requests")
} }
// ─── Tenant Symbol Visibility ───────────────────────────── // ─── Tenant Symbol Collection ─────────────────────────────
model TenantSymbol { model TenantSymbol {
id String @id @default(uuid()) id String @id @default(uuid())
isActive Boolean @default(true) customName String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
tenantId String tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
iconId String iconId String?
icon IconAsset @relation(fields: [iconId], references: [id], onDelete: Cascade) icon IconAsset? @relation(fields: [iconId], references: [id], onDelete: SetNull)
@@unique([tenantId, iconId]) // 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") @@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) ──────────── // ─── Dictionary (Global + Tenant word library) ────────────
model DictionaryEntry { model DictionaryEntry {

View File

@@ -44,11 +44,10 @@ async function main() {
patterns: ['Massstab', 'Nordrichtung', 'Windrichtung'] }, patterns: ['Massstab', 'Nordrichtung', 'Windrichtung'] },
] ]
// Delete ALL old system icons (regardless of fileKey pattern) // NOTE: We intentionally do NOT delete old system icons here.
const deleted = await prisma.iconAsset.deleteMany({ // TenantSymbol rows reference IconAsset.id via foreign key.
where: { isSystem: true }, // Deleting would either break references (tenant symbols become 404s)
}) // or cascade-delete tenant symbols. Instead we upsert by fileKey.
console.log(`🗑️ ${deleted.count} old system icons removed`)
// Upsert global categories (preserves tenant categories) // Upsert global categories (preserves tenant categories)
const catMap = {} const catMap = {}
@@ -90,6 +89,7 @@ async function main() {
} }
let created = 0 let created = 0
let updated = 0
for (const file of svgFiles) { for (const file of svgFiles) {
let name = file.replace('.svg', '') let name = file.replace('.svg', '')
name = name.replace(/_de$/i, '').replace(/_DE$/i, '').replace(/-de$/i, '') name = name.replace(/_de$/i, '').replace(/_DE$/i, '').replace(/-de$/i, '')
@@ -99,7 +99,21 @@ async function main() {
const category = findCategory(file) const category = findCategory(file)
const existing = await prisma.iconAsset.findFirst({ where: { fileKey } }) 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({ await prisma.iconAsset.create({
data: { data: {
name, 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() 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'] }, patterns: ['Massstab', 'Nordrichtung', 'Windrichtung'] },
] ]
// Delete ALL old system icons (regardless of fileKey pattern) // NOTE: We intentionally do NOT delete old system icons here.
const deletedIcons = await prisma.iconAsset.deleteMany({ // TenantSymbol rows reference IconAsset.id via foreign key.
where: { isSystem: true }, // Deleting would either break references (tenant symbols become 404s)
}) // or cascade-delete tenant symbols. Instead we upsert by fileKey.
console.log(`🗑️ ${deletedIcons.count} old system icons removed`)
// Clean up empty global categories // NOTE: We intentionally do NOT delete any icon categories here.
const oldGlobalCats = await prisma.iconCategory.findMany({ where: { tenantId: null } }) // Tenant-specific categories may reference them, and deleting could
for (const oldCat of oldGlobalCats) { // orphan user data. Empty categories are harmless.
const remaining = await prisma.iconAsset.count({ where: { categoryId: oldCat.id } }) // Create or update global categories
if (remaining === 0) {
await prisma.iconCategory.delete({ where: { id: oldCat.id } }).catch(() => {})
}
}
// Create new global categories
const catMap = {} const catMap = {}
for (const def of catDefs) { for (const def of catDefs) {
const cat = await prisma.iconCategory.upsert({ const cat = await prisma.iconCategory.upsert({
@@ -163,6 +157,7 @@ async function main() {
} }
let created = 0 let created = 0
let updated = 0
for (const file of svgFiles) { for (const file of svgFiles) {
// Clean name: remove .svg, remove _de/_DE suffix // Clean name: remove .svg, remove _de/_DE suffix
let name = file.replace('.svg', '') let name = file.replace('.svg', '')
@@ -173,7 +168,21 @@ async function main() {
const category = findCategory(file) const category = findCategory(file)
const existing = await prisma.iconAsset.findFirst({ where: { fileKey } }) 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({ await prisma.iconAsset.create({
data: { data: {
name, 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 // Create a demo project
const demoProject = await prisma.project.upsert({ const demoProject = await prisma.project.upsert({

View File

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

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="49.546932"
height="49.547974"
viewBox="0 0 49.546932 49.547974"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-16.9052,-3.7521347)">
<path
id="path1"
d="M 493.391,32.4492 H 131.789 V 394.059 h 361.602 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 493.391,32.4492 131.789,394.059"
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 131.789,32.4492 493.391,394.059"
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,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="68.655785"
height="41.02544"
viewBox="0 0 68.655785 41.02544"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-6.9854132,-8.0131316)">
<path
id="path1"
d="M 131.68,121.012 H 57.3906 V 308.328 H 131.68 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,56.96)" />
<path
id="path2"
d="m 206.98,64.4102 -74.449,56.6018 v 187.316 l 74.449,53.774 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,56.96)" />
<path
id="path3"
d="M 260.762,248.219 H 470.539"
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,56.96)" />
<path
id="path4"
d="M 260.762,178.289 H 470.539"
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,56.96)" />
<path
id="path5"
d="M 457.309,323.281 V 103.219 l 110,110.031 -110,110.031"
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,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="47.630798"
height="47.630772"
viewBox="0 0 47.630799 47.630772"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-17.853066,-4.607868)">
<path
id="path1"
d="M 486.129,40.4102 H 138.898 V 387.641 h 347.231 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 220.039,231.18 h 22.352 l -0.493,65.832 h 0.25 L 263.02,231.18 h 21 l 21.859,65.832 h 0.25 l -0.367,-65.832 h 22.347 v 85.718 H 293.48 l -19.031,-56.976 h -0.25 L 256.02,316.898 H 220.039 V 231.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 341.738,233.391 c 7.621,-2.45 17.571,-3.68 25.551,-3.68 19.031,0 37.699,6.019 37.699,28.238 0,30.949 -40.527,21.621 -40.527,35.5 0,7.492 9.578,7.981 15.348,7.981 6.511,0 13.14,-1.231 19.043,-3.93 l 1.468,17.68 c -5.769,1.968 -13.871,3.199 -22.101,3.199 -17.199,0 -37.34,-5.649 -37.34,-26.289 0,-31.192 40.531,-21.121 40.531,-37.078 0,-6.883 -6.879,-8.352 -14.121,-8.352 -9.461,0 -17.687,2.449 -23.578,5.641 l -1.973,-18.91"
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.7 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="58.059853"
height="15.795732"
viewBox="0 0 58.059853 15.795732"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-12.616146,-20.628268)">
<path
id="path1"
d="M 129.891,155.988 H 160.41 V 270.52 H 133.5 L 94.6211,245.25 108.238,224.578 129.891,240 v -84.012"
style="fill:#0a1323;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path2"
d="m 195.691,214.898 h 27.559 v 26.582 h -27.559 z m 0,-58.91 h 27.559 v 26.582 h -27.559 v -26.582"
style="fill:#0a1323;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,56.96)" />
<path
id="path3"
d="m 246.879,208.988 c 8.199,2.133 16.57,2.793 24.941,2.793 12.141,0 25.43,-3.941 25.43,-16.742 0,-14.598 -11.98,-18.367 -23.141,-18.367 -9.679,0 -19.187,1.969 -27.89,6.226 l -0.489,-24.769 c 9.348,-2.301 19.36,-4.109 30.68,-4.109 25.602,0 50.371,10.011 50.371,42.011 0,25.258 -20.511,35.438 -43.812,35.438 -1.969,0 -4.75,-0.328 -9.02,-0.66 v 19.031 h 48.403 v 20.68 h -74.653 l -0.82,-61.532"
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 385.191,154.02 c 37.899,0 44.469,29.539 44.469,59.23 0,29.699 -6.57,59.238 -44.469,59.238 -37.902,0 -44.461,-29.539 -44.461,-59.238 0,-29.691 6.559,-59.23 44.461,-59.23 z m 0,20.679 c -13.781,0 -14.929,20.34 -14.929,38.551 0,18.219 1.148,38.559 14.929,38.559 13.778,0 14.93,-20.34 14.93,-38.559 0,-18.211 -1.152,-38.551 -14.93,-38.551"
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 485.602,154.02 c 37.91,0 44.468,29.539 44.468,59.23 0,29.699 -6.558,59.238 -44.468,59.238 -37.903,0 -44.461,-29.539 -44.461,-59.238 0,-29.691 6.558,-59.23 44.461,-59.23 z m 0,20.679 c -13.782,0 -14.93,20.34 -14.93,38.551 0,18.219 1.148,38.559 14.93,38.559 13.789,0 14.937,-20.34 14.937,-38.559 0,-18.211 -1.148,-38.551 -14.937,-38.551"
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: 2.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="50.141731"
height="49.562546"
viewBox="0 0 50.141731 49.562546"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-16.519733,-3.8560013)">
<path
id="path1"
d="m 494.961,214.82 c 0,-99.898 -81.949,-180.8591 -183.039,-180.8591 -101.082,0 -183.024,80.9611 -183.024,180.8591 0,99.879 81.942,180.86 183.024,180.86 101.09,0 183.039,-80.981 183.039,-180.86 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,57.28)" />
<path
id="path2"
d="m 250.328,165.961 h 25.481 L 275.25,241 h 0.281 l 23.797,-75.039 H 323.27 L 348.191,241 h 0.278 l -0.418,-75.039 h 25.48 v 97.719 h -39.48 l -21.699,-64.961 h -0.282 l -20.718,64.961 h -41.024 v -97.719"
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.2 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="34.925465"
height="50.926559"
viewBox="0 0 34.925465 50.926559"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-24.138533,-2.9600013)">
<path
id="path1"
d="M 437.98,28.0508 H 186.039 V 400 H 437.98 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 312.012,260.211 V 275.02"
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 312.012,184.27 v 68.05"
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.3 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.030006"
height="28.917583"
viewBox="0 0 71.030007 28.917583"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-7.0159581,-14.121084)">
<path
id="path1"
d="M 581.078,318.629 453.262,109.41 H 188.5 L 56.8516,318.629"
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 508.789,202.039 H 129.148 l 58.293,-92.629 h 264.75 l 56.598,92.629"
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: 987 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="12.567733"
height="46.999985"
viewBox="0 0 12.567733 46.999985"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-35.292132,-4.9772013)">
<path
id="path1"
d="M 315.93,37.3711 V 303.488"
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 344.262,284.172 315.93,389.871 287.602,284.172 h 56.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="path3"
d="m 264.691,129.371 h 25.11 l -0.879,70.027 h 0.289 l 35.82,-70.027 h 33.918 v 102.481 h -24.961 l 0.442,-69.59 h -0.289 l -34.653,69.59 H 264.691 V 129.371"
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: 1.2 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="50.141197"
height="49.563999"
viewBox="0 0 50.141197 49.563999"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g1"
transform="translate(-16.504133,-3.6960013)">
<path
id="path1"
d="m 311.801,134.82 c 43.527,0 78.808,35.301 78.808,78.809 0,43.512 -35.281,78.781 -78.808,78.781 -43.492,0 -78.789,-35.269 -78.789,-78.781 0,-43.508 35.297,-78.809 78.789,-78.809"
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 494.84,213.621 c 0,-99.91 -81.942,-180.871 -183.039,-180.871 -101.071,0 -183.02,80.961 -183.02,180.871 0,99.879 81.949,180.859 183.02,180.859 101.097,0 183.039,-80.98 183.039,-180.859 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)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

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