arc42 Systemarchitektur — Yachtlogbuch
Stand: August 2026 · Gliederung nach arc42. Dieses Dokument beschreibt Bausteine und Abläufe auf der Ebene ihrer Verantwortlichkeiten, nicht auf Klassenebene — damit es ein Refactoring überlebt.
1. Einführung und Ziele
Das Yachtlogbuch ist eine Webanwendung, mit der Seglerinnen und Segler ein digitales Schiffstagebuch führen. Erfasst werden Reisen, ihre Gliederung in Törns, die einzelnen Segeltage und Tage für andere Aktivitäten sowie Wegpunkte mit Koordinate und Zeit. Aus diesen Daten erzeugt das System auf Abruf ein gesetztes PDF-Logbuch, dasselbe PDF als ZIP mit den Originalbildern, einen schlanken HTML-Export und — für Administratoren — einen Rohdatenexport.
1.1 Aufgabenstellung
| Kernfunktion | Beschreibung |
|---|---|
| Logbuchführung | Reisen, Törns, Segeltage, Tage für andere Aktivitäten und Wegpunkte anlegen, ändern, löschen |
| Wegpunkt-Ausprägungen | Unterscheidung zwischen GEPLANT, GETRACKT und BEOBACHTET |
| GPS-Tracking | Automatische Aufzeichnung getrackter Wegpunkte auf dem Mobilgerät |
| Bilder | Fotos an Segeltage, Tage für andere Aktivitäten und Wegpunkte hängen; Reise-Logo |
| Offline-Fähigkeit | Erfassen ohne Netz, Nachsenden bei Wiederverbindung |
| Export | PDF-Logbuch, PDF mit Originalbildern als ZIP, minimale Webseite, Excel-Rohdaten (nur Administratoren) |
| Mehrsprachigkeit | Oberfläche und Export in Deutsch, Englisch, Französisch, Italienisch |
1.2 Qualitätsziele
| Priorität | Qualitätsziel | Motivation |
|---|---|---|
| 1 | Datenhoheit | Was erfasst wurde, muss vollständig wieder herausgehen — als Logbuch und als Rohdaten. Ein Logbuch, das seine Daten einschliesst, ist wertlos. |
| 2 | Vertraulichkeit | Ein Logbuch enthält Bewegungsprofile. Fremde Reisen sind weder les- noch auffindbar; sie werden nach aussen als nicht existent behandelt. |
| 3 | Verfügbarkeit unterwegs | Erfassung muss ohne Netzverbindung funktionieren, weil genau dort erfasst wird, wo keines ist. |
| 4 | Nachvollziehbarkeit | Geplant, getrackt und beobachtet bleiben dauerhaft unterscheidbar, damit Absicht und Wirklichkeit vergleichbar sind. |
| 5 | Betreibbarkeit durch eine Person | Das System wird von einer einzelnen Person betrieben. Deployment und Rollback müssen in wenigen Minuten erledigt sein. |
1.3 Stakeholder
| Rolle | Erwartungshaltung |
|---|---|
| Skipper / Anwender | Erfasst unterwegs, oft auf dem Mobilgerät, oft ohne Netz. Erwartet, dass nichts verloren geht. |
| Crew und Familie | Liest mit, ohne Konto — über den HTML- oder PDF-Export. |
| Administrator | Betreibt das System, exportiert Rohdaten, verantwortet Sicherung und Deployment. |
| Entwickler | Eine Person. Braucht ein System, das ohne Spezialwissen wieder aufgegriffen werden kann. |
2. Randbedingungen
2.1 Technische Randbedingungen
| Randbedingung | Erläuterung |
|---|---|
| Java 21, Spring Boot 4.1 | Alle drei Backend-Dienste; Maven-Multi-Modul-Projekt mit gemeinsamem Parent-POM |
| PostgreSQL 17 | Einzige relationale Datenbank; Schema ausschliesslich über Flyway-Migrationen |
| Keycloak 26.1 | Einziger Identitätsanbieter; die Dienste sind reine OAuth2-Resource-Server |
| React 19 / TypeScript / Vite | Frontend als Single-Page-Anwendung, ausgeliefert von nginx |
| S3-kompatibler Objektspeicher | Bilder liegen ausserhalb der Datenbank; lokal MinIO |
| Docker Compose | Einzige Betriebsform, lokal wie produktiv |
| Ein Deployment-Ziel | Eine Hetzner-VM, verwaltet über Dokploy |
2.2 Organisatorische Randbedingungen
- Ein Entwickler, eine Betriebsumgebung. Es gibt keine Staging-Stufe; die Absicherung erfolgt über Integrations- und End-to-End-Tests vor dem Deployment.
- API-First. Die REST-Schnittstelle des
yachtlogbuch-serviceist inapi/yacht-logbuch-api.yaml(OpenAPI 3.0.3) spezifiziert; DTOs für Backend und Frontend werden daraus generiert. Die Spezifikation wird zuerst geändert, nicht der Code. - Deutsche Fachsprache im Code. Domänenbegriffe (
Reise,Toern,Segeltag,Wegpunkt) erscheinen unübersetzt in Klassennamen, Tabellen und API-Pfaden.
2.3 Konventionen
| Bereich | Konvention |
|---|---|
| Fehler | DomainException mit stabilem ErrorCode; die Codes sind Teil des API-Vertrags |
| Löschen | Kaskadierende Löschung aufwärts statt JPA-Cascade abwärts, gebündelt im DeletionService |
| Tests | *Test als Unit-Test (Surefire, kein Docker), *IT als Integrationstest (Failsafe, Testcontainers) |
| Zeitstempel | Intern Instant in UTC, an der API OffsetDateTime |
3. Kontextabgrenzung
3.1 Fachlicher Kontext
| Partner | Eingehend | Ausgehend |
|---|---|---|
| Skipper | Logbuchdaten, Bilder, GPS-Positionen | Darstellung, Karten, Exporte |
| Administrator | Betriebseingriffe | Excel-Rohdatenexport, Actuator-Kennzahlen |
| Crew und Familie | — | PDF-Logbuch, HTML-Export (ohne Konto) |
| Kartendienst | Kartenkacheln, statische Kartenbilder | Koordinatenausschnitte |
3.2 Technischer Kontext
| Schnittstelle | Protokoll | Beschreibung |
|---|---|---|
| Browser ↔ nginx | HTTPS | Auslieferung von Startseite, Dokumentation und Anwendung; Reverse Proxy für alle API-Aufrufe |
Frontend ↔ yachtlogbuch-service |
REST/JSON über /app/api/v1 |
Logbuchdaten |
Frontend ↔ picture-service |
REST/multipart über /app/pictures |
Bild-Upload und -Abruf |
Frontend ↔ export-service |
REST über /app/export |
PDF-, ZIP-, HTML- und Excel-Export |
| Frontend ↔ Keycloak | OpenID Connect, Authorization Code mit PKCE | Anmeldung, Token-Erneuerung |
| Dienste ↔ Keycloak | JWKS über HTTP | Signaturprüfung der Zugriffstoken |
| Dienste ↔ PostgreSQL | JDBC | Persistenz |
picture-service ↔ Objektspeicher |
S3-API | Bilder und Thumbnails |
Sämtlicher Browserverkehr läuft über eine einzige Herkunft (https://yachtlogbuch.ch). Damit
entfällt CORS im Produktionsbetrieb vollständig — eine bewusste Entscheidung, siehe Kapitel 9.
4. Lösungsstrategie
| Qualitätsziel | Lösungsansatz |
|---|---|
| Datenhoheit | Ein eigener export-service als dritter Baustein, der die Exporte serverseitig rendert. Der Rohdatenexport gibt zusätzlich alle Wegpunkte heraus — auch die getrackten, die im Logbuch-Export bewusst ausgeblendet bleiben. |
| Vertraulichkeit | Jede Reise trägt einen Besitzer (besitzer_subject aus dem JWT). Fremde Daten liefern 404, nicht 403 — die Existenz fremder Reisen wird nicht preisgegeben. |
| Verfügbarkeit unterwegs | Schreibzugriffe werden im Browser in IndexedDB zwischengespeichert und beim Wiederverbinden in Reihenfolge nachgesendet; Lesedaten liegen als Snapshot-Cache vor. |
| Nachvollziehbarkeit | WegpunktStatus mit den drei Werten GEPLANT, GETRACKT, BEOBACHTET ist Teil des Datenmodells, nicht eine Sichtfilterung. |
| Betreibbarkeit | Fünf Container in einem Compose-Verbund, Deployment über einen Image-Tag. Rollback ist das Zurücksetzen dieses Tags. |
Die leitenden Entscheidungen im Überblick — ausführlich in Kapitel 9:
- Schnitt in drei Backend-Dienste entlang der Ressourcenarten, nicht entlang der Fachlichkeit. Logbuchdaten, Binärdaten und Renderarbeit haben grundverschiedene Lastprofile.
- API-First mit generierten DTOs auf beiden Seiten. Die OpenAPI-Datei ist der Vertrag.
- Eine einzige Herkunft im Browser. nginx liefert Startseite, Dokumentation und Anwendung aus
und proxied alle Dienste unter
/app/…. - Zustandslose Dienste. Kein Session-Speicher, keine Sticky Sessions; Autorisierung ausschliesslich aus dem Token.
5. Bausteinsicht
5.1 Ebene 1 — Gesamtsystem
yachtlogbuch-frontend
Aufgabe. Zwei Dinge in einem Container. Erstens nginx als einziger Einstiegspunkt: Es liefert die
öffentliche Startseite (/, vier Sprachfassungen), diese Dokumentation (/docs/) und die
Single-Page-Anwendung (/app/) aus und reicht alle API-Aufrufe an die Backend-Dienste weiter.
Zweitens die React-Anwendung selbst — Erfassungsmasken, Karten, GPS-Tracking, Offline-Warteschlange.
Schnittstellen. Nach aussen HTTPS. Nach innen HTTP zu allen drei Backend-Diensten. Zu Keycloak per OpenID Connect aus dem Browser heraus. Das Keycloak-Login-Theme entsteht aus demselben Quellbaum (Keycloakify), damit Anmeldemaske und Anwendung dasselbe Erscheinungsbild haben.
yachtlogbuch-service
Aufgabe. Führt die Logbuchdaten und ist alleiniger Eigentümer des Logbuch-Schemas. Setzt die fachlichen Regeln durch: Törns liegen innerhalb ihrer Reise und überlappen einander nicht, Segeltage liegen im Zeitraum ihres Törns, das Löschen des letzten Kindes räumt den Elternteil mit auf. Setzt den Besitzer einer Reise serverseitig aus dem Token und filtert jeden Zugriff darüber.
Schnittstellen. REST/JSON unter /api/v1, spezifiziert in api/yacht-logbuch-api.yaml. JDBC zur
Datenbank. HTTP zum picture-service, um beim Löschen einer Entität deren Bilder mitzuentfernen.
JWKS-Abruf bei Keycloak zur Tokenprüfung.
picture-service
Aufgabe. Nimmt Bilder entgegen, erzeugt Thumbnails und legt beides im Objektspeicher ab. Hält
bewusst keine fachliche Kenntnis — er weiss nichts von Reisen oder Wegpunkten, sondern verwaltet
Bilder unter einer eigenen Kennung. Die Zuordnung zu Segeltagen, Tagen für andere Aktivitäten und
Wegpunkten liegt beim yachtlogbuch-service.
Schnittstellen. REST unter /pictures (Upload als multipart/form-data, Abruf von Metadaten,
Original und Thumbnail, Löschen). S3-API zum Objektspeicher. JWKS-Abruf bei Keycloak.
export-service
Aufgabe. Erzeugt aus verteilten Daten ein Dokument. Er hält selbst keine Daten, sondern
aggregiert zur Laufzeit: Logbuchdaten vom yachtlogbuch-service, Bilder vom picture-service,
Kartenausschnitte vom Kartendienst. Daraus entstehen drei Ergebnisse — ein gesetztes PDF, ein
schlanker HTML-Export und das Excel-ZIP mit Rohdaten.
Schnittstellen. REST unter /export; die PDF- und HTML-Variante unterscheiden sich nur im
Accept-Header derselben Ressource. HTTP zu den beiden anderen Diensten und zum Kartendienst.
JWKS-Abruf bei Keycloak.
Keycloak
Aufgabe. Einziger Identitätsanbieter, Realm yachtlogbuch. Vergibt die Realm-Rollen
logbuch-user und logbuch-admin. Das Anmelde-Theme ist Teil des Deployments und wird als eigenes
Image gebaut.
PostgreSQL und Objektspeicher
Zwei getrennte PostgreSQL-Instanzen: eine für das Logbuch-Schema, eine für Keycloak. Die Trennung
hält Keycloak-Upgrades von den fachlichen Migrationen fern. Das Logbuch-Schema wird ausschliesslich
über Flyway verändert; Hibernate läuft mit ddl-auto: validate und darf nichts anlegen. Bilder
liegen im Objektspeicher, nie in der Datenbank.
5.2 Ebene 2 — Innenansicht yachtlogbuch-service
| Paket | Verantwortung |
|---|---|
controller |
REST-Endpunkte, Eingabevalidierung, einheitliche Fehlerabbildung im GlobalExceptionHandler |
domain |
JPA-Entities und fachliche Aufzählungstypen (WegpunktStatus, Wetterlage, Manoevertyp, Einheiten) |
repository |
Spring-Data-Repositories; Besitzerfilterung als Teil der Abfragen |
mapper |
MapStruct zwischen Entity und generiertem DTO |
service |
DeletionService für die aufwärts kaskadierende Löschung, PictureServiceClient für das Mitlöschen von Bildern |
config |
SecurityConfig (OAuth2-Resource-Server, Rollenabbildung), JpaConfig (Auditing), OpenApiConfig |
security |
AdminAccess — gebündelte Prüfung der Administratorrolle |
exception |
DomainException mit stabilem ErrorCode; die Codenamen sind Teil des API-Vertrags |
Die Löschlogik ist bewusst nicht als JPA-Cascade modelliert. Fachlich gilt die Gegenrichtung: Wer den letzten Wegpunkt eines Segeltags entfernt, entfernt damit auch den Segeltag, dessen Törn und gegebenenfalls die Reise. Diese Regel ist an einer Stelle nachlesbar statt über Annotationen verstreut.
5.3 Ebene 2 — Innenansicht export-service
| Paket | Verantwortung |
|---|---|
client |
YachtlogbuchClient, PictureClient, StaticMapClient — die drei Datenquellen |
aggregat |
ReiseAggregat, SegeltagAggregat, ZeitleistenEintrag — die zusammengeführte Sicht, aus der gerendert wird |
service |
ExportAggregationService (Beschaffung), PdfRenderService, HtmlRenderService, EnumTranslationService |
service.excel |
ExcelExportService, ExcelSheetWriter, SequentialIdMapper für den Rohdatenexport |
controller |
ExportController, GlobalExceptionHandler |
Der ZeitleistenEintrag ist der Schlüssel zum Logbuch-Export: Segeltage und Tage für andere
Aktivitäten werden zu einer einzigen chronologischen Folge verschmolzen, sodass im Dokument keine
unerklärten Lücken entstehen.
Der SequentialIdMapper verdient eine eigene Erwähnung. Intern identifizieren UUIDs die Entitäten —
in einer Excel-Tabelle sind die unbrauchbar. Der Mapper vergibt deshalb je Entitätstyp fortlaufende
Nummern ab 1 und hält sie über alle Dateien eines Exportlaufs konsistent, sodass Fremdschlüssel
zwischen den Blättern lesbar bleiben.
5.4 Ebene 2 — Innenansicht Frontend
| Bereich | Verantwortung |
|---|---|
api/generated |
Aus der OpenAPI-Datei erzeugter Client; wird nicht von Hand geändert |
auth |
AuthContext (Keycloak-Anbindung, Tokenerneuerung), ProtectedRoute |
pages / components |
Seiten je Entitätstyp und die zugehörigen Formulare, Karten und Modale |
offline |
IndexedDB-Anbindung, Warteschlange für Schreibzugriffe, Snapshot-Cache, Online-Status |
i18n |
Vier Sprachen, geladen über i18next |
login / kc.gen |
Keycloakify-Anmeldetheme aus demselben Quellbaum |
site/ |
Startseite und Dokumentation — bewusst ausserhalb der Anwendung, siehe Kapitel 9 |
Das Paket offline ist der anspruchsvollste Teil des Frontends. Der entscheidende Kunstgriff: Die
Kennung einer neuen Entität wird bereits im Browser als UUID vergeben, nicht erst vom Server. Damit
kann ein Wegpunkt auf einen Segeltag verweisen, der noch in der Warteschlange liegt — es gibt keine
vorläufigen Kennungen, die später ersetzt werden müssten. Die Warteschlange selbst liegt je Benutzer
in IndexedDB, wird in Erfassungsreihenfolge abgearbeitet und zählt Fehlversuche je Eintrag mit.
6. Laufzeitsicht
6.1 Anmeldung
Die Anwendung ist ein öffentlicher OAuth2-Client und nutzt den Authorization-Code-Flow mit PKCE. Ein Client-Geheimnis gibt es nicht — es liesse sich in einer Browseranwendung ohnehin nicht verbergen.
- Der Browser ruft
/app/auf. Die Anwendung startet und fragt Keycloak über eine unsichtbare Prüfseite, ob bereits eine Sitzung besteht. - Besteht keine, leitet sie zu Keycloak weiter. Dort erscheint die Anmeldemaske im Yachtlogbuch-Erscheinungsbild — dasselbe Theme, aus demselben Quellbaum gebaut.
- Nach erfolgreicher Anmeldung kehrt der Browser mit einem Autorisierungscode zurück, den die Anwendung gegen ein Zugriffs- und ein Erneuerungstoken tauscht.
- Jeder API-Aufruf trägt das Zugriffstoken im
Authorization-Header. Die Dienste prüfen dessen Signatur gegen den öffentlichen Schlüssel von Keycloak und lesen die Realm-Rollen aus dem Claimrealm_access.roles; daraus entstehen Spring-Security-Berechtigungen mit dem PräfixROLE_. - Vor Ablauf erneuert die Anwendung das Token im Hintergrund.
Die Dienste halten dabei keinerlei Sitzungszustand. Sie sind vollständig zustandslos konfiguriert; alles, was für eine Autorisierungsentscheidung nötig ist, steht im Token.
6.2 Wegpunkt mit Bild erfassen
Dieser Ablauf berührt alle drei Backend-Dienste und zeigt zugleich, warum das Bild vor dem Wegpunkt gespeichert wird.
- Der Anwender füllt das Wegpunkt-Formular aus und wählt ein Foto.
- Das Frontend lädt zuerst das Bild zum
picture-service(POST /pictures). Der Dienst legt Original und erzeugtes Thumbnail im Objektspeicher ab und antwortet mit einer Bildkennung. - Erst danach sendet das Frontend den Wegpunkt an den
yachtlogbuch-service— mit der Bildkennung als Verweis. - Der
yachtlogbuch-serviceprüft die fachlichen Regeln (Koordinatenbereich, Zugehörigkeit zum Segeltag, Besitzverhältnis) und speichert.
Die Reihenfolge ist Absicht: Ein Bild ohne Wegpunkt ist ein verwaister Blob, der beim nächsten Aufräumen verschwindet. Ein Wegpunkt, der auf ein nicht existierendes Bild zeigt, wäre dagegen ein dauerhafter Datenfehler.
Im Offline-Fall verschiebt sich der Ablauf: Die Anwendung vergibt die Kennung des Wegpunkts selbst, legt den Schreibzugriff in der IndexedDB-Warteschlange ab und zeigt ihn sofort in der Oberfläche an. Als offline gilt dabei nicht nur ein gemeldeter Netzverlust, sondern auch eine Anfrage, die ohne Antwort bleibt — siehe Konzept 8.2. Kommt die Verbindung zurück, arbeitet der Warteschlangen-Versand die Einträge in Erfassungsreihenfolge ab. Weil die Kennungen bereits feststehen, bleiben Verweise zwischen nachgesendeten Objekten gültig.
Beim GPS-Tracking entsteht derselbe Wegpunkt automatisch, mit Status GETRACKT und ohne
Anwenderinteraktion; die Positionen liefert die Geolocation-Schnittstelle des Geräts. Jeder Punkt
durchläuft denselben Weg sofort nach seiner Aufzeichnung — online direkt zum Dienst, offline in die
Warteschlange. Er wird also nicht bis zum Beenden im flüchtigen Speicher der Seite gesammelt.
Der Grund ist Verlustsicherheit: Ein Reload, ein geschlossener Tab oder ein vom Betriebssystem verworfener Hintergrundtab hätte sonst die gesamte bisherige Aufzeichnung eines Segeltags vernichtet. Nebenbei verteilt sich die Offline-Warteschlange über die Aufzeichnungsdauer, statt am Ende in einem Schwall zu entstehen. Das Beenden stellt daher nur die Positionsüberwachung ein und speichert nichts nach; ein Reload beendet die Aufzeichnung, ohne sie selbsttätig fortzusetzen, und ein erneuter Start im selben Segeltag hängt weitere Punkte an den bestehenden Bestand an. Scheitert ein einzelner Punkt, läuft die Aufzeichnung weiter — der Fehler blockiert die folgenden Punkte nicht.
Wachbleiben ist Voraussetzung, nicht Komfort. Bei ausgeschaltetem Display stellen mobile Browser
watchPosition ein und drosseln Timer; es entstünden also weder Punkte noch Token-Erneuerungen, und
die Sitzung liefe im Hintergrund ab. Während einer Aufzeichnung fordert die Anwendung deshalb eine
Bildschirmsperre (navigator.wakeLock) an und erneuert sie nach jeder Rückkehr in den Vordergrund,
weil der Browser sie im Hintergrund entzieht. Beim Sichtbarwerden wird zusätzlich das Token aktiv
erneuert und die Warteschlange abgearbeitet. Eine Aufzeichnung bei dunklem Display bleibt damit
ausserhalb der Möglichkeiten einer Webanwendung — dafür wäre die native Hülle nötig.
6.3 Reise exportieren
- Der Anwender löst den Export einer Reise aus. Das Frontend ruft
/app/export/reisen/{id}auf und legt über denAccept-Header fest, ob ein PDF oder HTML gewünscht ist — dieselbe Ressource, zwei Darstellungen. Für das PDF samt Originalbildern gibt es die eigene Unterressource/app/export/reisen/{id}/zip. - Der
export-servicebeschafft die Daten: die Reise mit allen Törns, Segeltagen, Tagen für andere Aktivitäten und Wegpunkten vomyachtlogbuch-service, die zugehörigen Bilder vompicture-service, die Kartenausschnitte vom Kartendienst. Das Zugriffstoken des Anwenders wird dabei weitergereicht, sodass die Besitzprüfung desyachtlogbuch-serviceauch für den Export greift. - Die Aggregate werden zu einer Zeitleiste verschmolzen: Segeltage und Tage für andere Aktivitäten stehen chronologisch in einer Folge.
- Daraus rendert der Dienst das Ergebnis — ein gesetztes PDF oder eine in sich geschlossene
HTML-Datei. Wegpunkte mit Status
GETRACKTbleiben im Logbuch-Export aussen vor: Sie sind Messpunkte, keine Logbucheinträge, und würden das Dokument fluten. Ihre Information steckt stattdessen in der gezeichneten Strecke. - Der Browser erhält die fertige Datei zum Herunterladen.
Bilder im PDF. Der eingesetzte Renderer wertet max-width/max-height nicht aus; ein Bild würde
sonst in Pixelgrösse gezeichnet und über mehrere Seiten zerschnitten. Der Dienst liest deshalb die
Abmessungen aus dem Bild-Header und gibt jedem Bild eine ausgerechnete, seitenverhältnistreue Grösse
mit. Geladen wird für das PDF das Thumbnail des picture-service (max. 300 × 300 px) statt des
Originals — das hält das Dokument klein. Der HTML-Export bleibt bei den Originalen, weil dort keine
Seitenaufteilung stört.
ZIP-Variante. /zip lädt zusätzlich die Originale, benennt sie fortlaufend (bilder/001.jpg)
und packt sie neben das PDF. Die Namen stehen fest, bevor gerendert wird, denn das Template verweist
relativ auf sie und schreibt sie unter jedes Bild — nicht jeder Betrachter löst solche Verweise auf.
Die Originale werden dafür parallel geladen.
6.4 Rohdatenexport durch einen Administrator
Ein eigener Ablauf, weil er anderen Regeln folgt. Der Aufruf von /export/reisen/{id}/excel ist
Administratoren mit der Rolle logbuch-admin vorbehalten; alle anderen erhalten 403. Der Dienst
erzeugt ein ZIP mit je einer Excel-Datei für Reise, Törns, Segeltage, Tage für andere Aktivitäten und
Wegpunkte, dazu die Bilder und eine Indexdatei. Anders als der Logbuch-Export enthält er alle
Wegpunkte einschliesslich der getrackten — hier geht es um Vollständigkeit, nicht um Lesbarkeit.
Fremdschlüssel verweisen auf die fortlaufenden Nummern innerhalb desselben Exports, nicht auf die
internen UUIDs.
6.5 Löschen mit Aufwärtskaskade
Löschen ist im Yachtlogbuch nicht die Umkehrung des Anlegens. Wird der letzte Wegpunkt eines
Segeltags entfernt, verliert der Segeltag seinen Inhalt und wird mitgelöscht; ist er der letzte
Segeltag seines Törns, trifft es auch den Törn, und so weiter bis zur Reise. Der DeletionService
bündelt diese Regel an einer Stelle. Bilder der betroffenen Entitäten werden dabei über den
PictureServiceClient im picture-service mitentfernt, damit im Objektspeicher nichts zurückbleibt.
7. Verteilungssicht
7.1 Produktivbetrieb
Das System läuft auf einer einzelnen Hetzner-VM. Die Container werden über Dokploy verwaltet, das
einen Traefik als Reverse Proxy davorsetzt und die TLS-Zertifikate hält. Nach aussen existiert genau
ein Name: yachtlogbuch.ch.
| Knoten | Inhalt | Persistenz |
|---|---|---|
| Traefik (von Dokploy gestellt) | TLS-Terminierung, Routing auf den Frontend-Container | — |
yachtlogbuch-frontend |
nginx mit Startseite, Dokumentation, SPA und Proxy-Regeln | zustandslos |
yachtlogbuch-service |
Logbuch-API, Port 8080 | Datenbank db |
picture-service |
Bild-API, Port 8090 | Objektspeicher |
export-service |
Export-API, Port 8095 | zustandslos |
keycloak |
Identitätsanbieter samt Theme und Realm-Import | Datenbank postgres-keycloak |
db, postgres-keycloak |
PostgreSQL 17 | benannte Docker-Volumes |
| Objektspeicher | S3-kompatibel | benanntes Docker-Volume |
Entscheidend für die Sicherheit dieser Aufstellung: Nur der Frontend-Container ist mit dem Proxy-Netz verbunden. Datenbanken, Objektspeicher und die drei Backend-Dienste liegen ausschliesslich im internen Compose-Netz und haben von aussen keine erreichbare Adresse. Jeder Zugriff auf die APIs läuft zwangsläufig über nginx.
7.2 Wege durch nginx
| Pfad | Ziel |
|---|---|
/ |
Startseite — je nach Cookie und Browsersprache eine der vier Sprachfassungen, oder Weiterleitung nach /app/ für Wiederkehrer |
/de/, /en/, /fr/, /it/ |
die jeweilige Sprachfassung der Startseite |
/docs/arc42/, /docs/handbuch/ |
diese Dokumentation |
/site/ |
Stylesheet und übrige Assets der statischen Seiten |
/app/ |
die Single-Page-Anwendung |
/app/api/ |
Proxy auf yachtlogbuch-service:8080 |
/app/pictures |
Proxy auf picture-service:8090 |
/app/export |
Proxy auf export-service:8095 |
Keycloak liegt unter /auth/ derselben Domain, wird dorthin aber von Traefik geroutet und nicht von
nginx bedient.
Weil alles unter derselben Herkunft liegt, entfällt CORS vollständig. Unbekannte Pfade werden nach
/app/… weitergeleitet, damit Deep-Links auf die Anwendung auch dann funktionieren, wenn der
/app-Präfix fehlt; unterhalb von /docs/ gilt das ausdrücklich nicht — dort antwortet nginx mit
404, statt in die Anwendung umzulenken.
7.3 Deployment
Der Ablauf ist bewusst knapp gehalten und in DEPLOYMENT.md im Detail beschrieben:
- Push auf
main. GitHub Actions führtmvn verifysowie Lint und Unit-Tests des Frontends als Gate aus und baut anschliessend fünf Images nachghcr.io, jeweils mit den Tags:latestund:<commit-sha>. - In Dokploy wird die Variable
IMAGE_TAGauf den Commit-SHA gesetzt. - Deploy auslösen.
Der zweite Schritt ist nicht optional, sondern der eigentliche Mechanismus. Ein blosses „Redeploy"
führt lediglich docker compose up -d aus; da sich die Service-Definitionen dabei nicht ändern,
sieht Compose keinen Grund, neue Images zu ziehen — die Container bleiben unangetastet, auch wenn die
CI gerade ein neues :latest gepusht hat. Erst das Ändern von IMAGE_TAG ist eine echte Änderung
und erzwingt Pull und Neuerstellung.
Derselbe Mechanismus ist zugleich die Rollback-Strategie: IMAGE_TAG auf den vorherigen SHA setzen,
erneut deployen. Solange keine Datenbankmigration dazwischenliegt, ist der Rücksprung vollständig.
Migrationen sind deshalb additiv zu halten.
7.4 Entwicklungsumgebung
docker compose up -d --build --wait im Projektwurzelverzeichnis startet denselben Verbund lokal.
Das Frontend ist dann unter http://localhost:3000 erreichbar, die API unter :8080, Keycloak unter
:8180, der Objektspeicher unter :9000. Die Wege durch nginx sind lokal dieselben wie produktiv —
gerade deshalb können die End-to-End-Tests die Routing-Regeln überhaupt prüfen.
Für schnelle Frontend-Iteration läuft Vite alternativ direkt auf dem Rechner
(npm run dev, Port 5173). In dieser Betriebsart werden die Backend-Ports direkt angesprochen; dort
ist kein CORS konfiguriert, weshalb sie nur für reine Oberflächenarbeit taugt.
8. Querschnittskonzepte
8.1 Authentifizierung und Autorisierung
Keycloak ist der einzige Ort, an dem Identitäten liegen. Die drei Backend-Dienste sind reine OAuth2-Resource-Server: Sie prüfen die Signatur des Zugriffstokens gegen den öffentlichen Schlüssel des Realms und leiten daraus die Berechtigungen ab. Kein Dienst kennt Passwörter, keiner führt eine eigene Benutzertabelle.
| Rolle | Bedeutung |
|---|---|
logbuch-user |
Regulärer Zugriff auf die API — ausschliesslich auf eigene Daten |
logbuch-admin |
Zusätzlich Actuator-Endpunkte und der Excel-Rohdatenexport |
Autorisierung auf Datenebene. Rollen allein genügen nicht, weil alle Anwender dieselbe Rolle
tragen. Jede Reise trägt deshalb den subject-Claim ihres Erzeugers als Besitzer. Der Wert wird
beim Anlegen serverseitig gesetzt und ignoriert einen abweichenden Wert im Request-Body. Jede
Abfrage filtert darüber, und zwar auch für die abhängigen Entitäten: Ein Törn, Segeltag oder
Wegpunkt gehört dem Besitzer seiner Reise.
Antwort auf fremde Daten ist 404, nicht 403. Das ist eine bewusste Entscheidung: Ein 403
würde bestätigen, dass es die angefragte Reise gibt. Bei einem Logbuch — also einem Bewegungsprofil —
ist schon diese Bestätigung zu viel.
Sitzungsdauer folgt dem Nutzungsprofil, nicht dem Standardwert. Keycloaks Voreinstellung lässt
eine Sitzung nach 30 Minuten ohne Erneuerung verfallen. Für einen Segeltag ist das zu kurz: Wer eine
halbe Stunde ein Manöver fährt, während das Gerät in der Halterung steckt, müsste sich danach neu
anmelden. Der Realm setzt deshalb ssoSessionIdleTimeout auf 12 Stunden und ssoSessionMaxLifespan
auf 24 Stunden. Zu beachten: Ein Realm-Import überschreibt einen bestehenden Realm nicht — die Werte
müssen bei einer Änderung auch am laufenden Realm gesetzt werden.
Ein Sitzungsende leitet nicht um. Lässt sich das Zugriffstoken nicht erneuern, zeigt die Anwendung ein Hinweisband mit der Möglichkeit zur Neuanmeldung, statt sofort zur Anmeldeseite zu springen. Ein Redirect würde die Seite verlassen und dabei Formulareingaben sowie eine laufende GPS-Aufzeichnung verlieren — den Zustand also gerade dann vernichten, wenn er am schwersten wiederherzustellen ist. Erfasste Änderungen warten so lange in der Warteschlange. Ein fehlgeschlagener Erneuerungsversuch ohne Netz gilt dabei nicht als Sitzungsende, sondern nur als misslungener Versuch.
8.2 Offline-Fähigkeit
Erfasst wird dort, wo es kein Netz gibt. Das Frontend behandelt Netzverlust deshalb nicht als Fehler, sondern als Normalzustand.
- Lesen bedient sich aus einem Snapshot-Cache in IndexedDB.
- Schreiben landet in einer Warteschlange, ebenfalls in IndexedDB, je Benutzer getrennt und nach Erfassungszeit geordnet.
- Kennungen entstehen bereits im Browser als UUID. Deshalb kann ein Wegpunkt auf einen Segeltag verweisen, der noch gar nicht gesendet wurde.
- Nachsenden geschieht automatisch beim Wiederverbinden, in Erfassungsreihenfolge, mit Zählung der Fehlversuche je Eintrag. Zusätzlich beim Zurückkehren aus dem Hintergrund, weil mobile Browser Timer und Ereignisse dort aussetzen.
- Sichtbarkeit: Ein Banner zeigt den Offline-Zustand und die Zahl ausstehender Änderungen an.
Nicht navigator.onLine entscheidet, sondern die Zustellung. Das Flag meldet nur, ob eine
Netzwerkschnittstelle besteht — bei schwachem Mobilfunk am Wasser also praktisch immer „online",
während Anfragen in Timeouts laufen. Eine Änderung wandert deshalb auch dann in die Warteschlange,
wenn sie abgeschickt wurde und keine Antwort erhielt. Ein 401 zählt ebenfalls dazu: Die Sitzung ist
erneuerbar, die Daten sollen das überleben. Aus demselben Grund verwirft der Warteschlangen-Versand
einen Eintrag bei 401/403 nicht, sondern lässt ihn bis nach der Neuanmeldung liegen — anders als
bei einem fachlichen 4xx, das sich durch Wiederholung nicht ändert.
Die Grenze dieses Ansatzes ist bewusst gezogen: Es gibt keine Konfliktauflösung für konkurrierende Änderungen. Bei einem persönlichen Logbuch mit einem Erfasser je Reise ist der Fall selten genug, dass die Komplexität einer Auflösung nicht gerechtfertigt wäre.
8.3 Mehrsprachigkeit
Vier Sprachen — Deutsch, Englisch, Französisch, Italienisch — an drei Stellen:
| Ort | Umsetzung |
|---|---|
| Anwendung | i18next mit Sprachdateien je Locale |
| Export | Der export-service übersetzt Aufzählungswerte über den EnumTranslationService und rendert das Dokument in der gewählten Sprache |
| Startseite | Vier zur Bauzeit vorgerenderte HTML-Fassungen; die Auswahl trifft nginx |
Fachbegriffe der Domäne bleiben im Code deutsch. Übersetzt wird an der Oberfläche, nicht im Datenmodell.
8.4 Bilder
Bilder liegen nie in der Datenbank. Der picture-service legt Original und ein erzeugtes Thumbnail
im Objektspeicher ab und gibt eine Kennung zurück; die fachliche Zuordnung hält der
yachtlogbuch-service als Verweis. Das hält die Datenbank klein und die Sicherung überschaubar.
Dass beide Varianten vorliegen, nutzt der Export: das PDF bindet das Thumbnail ein und bleibt damit handlich, während die Originale nur in der ZIP-Variante mitgeliefert werden (siehe Laufzeitsicht 6.3).
Der Preis: Beim Löschen müssen zwei Systeme mitspielen. Der DeletionService ruft deshalb über den
PictureServiceClient das Entfernen der Bilder an. Schlägt das fehl, bleibt ein verwaister Blob im
Objektspeicher zurück — unschön, aber harmlos, und dem umgekehrten Fall eines Verweises ins Leere
klar vorzuziehen.
Das Reise-Logo folgt einem eigenen, älteren Weg: Es liegt im Dateisystem des yachtlogbuch-service,
die Metadaten in der Tabelle reise_logo. Diese Ungleichheit ist historisch gewachsen und in
Kapitel 11 als Risiko vermerkt.
8.5 Fehlerbehandlung
Fachliche Fehler werden als DomainException mit einem ErrorCode aus einer festen Aufzählung
geworfen. Ein GlobalExceptionHandler bildet sie auf HTTP-Status und eine einheitliche
Fehlerstruktur ab. Die Codenamen sind Teil des API-Vertrags und müssen mit der Aufzählung in der
OpenAPI-Datei übereinstimmen — das Frontend entscheidet anhand des Codes, welche Meldung es zeigt,
nicht anhand des Textes.
8.6 Persistenz und Migration
Das Schema gehört Flyway. Hibernate läuft mit ddl-auto: validate und darf nichts anlegen oder
ändern; weicht das Schema von den Entities ab, startet der Dienst nicht. Damit ist ausgeschlossen,
dass eine Schemaänderung unbemerkt in Produktion gerät.
Migrationen sind additiv zu halten, weil das Rollback über den Image-Tag läuft und eine ältere Anwendungsversion mit dem neueren Schema starten können muss.
8.7 Vereinfachung getrackter Routen
Ein Segeltag mit kurzem Aufzeichnungsintervall liefert Hunderte bis Tausende Wegpunkte, von denen die meisten auf gerader Strecke liegen und keine zusätzliche Information tragen. Anwender können eine aufgezeichnete Route deshalb nachträglich ausdünnen lassen.
Verfahren. Ramer–Douglas–Peucker über die nach Reihenfolge sortierte Punktfolge, implementiert in
TrackSimplifier im yachtlogbuch-service. Der Algorithmus läuft iterativ mit explizitem Stack —
bei mehreren tausend Punkten wäre Rekursion ein Stapelüberlauf-Risiko. Abstände werden in einer
äquirektangulären Projektion um die mittlere Breite des Tracks in Metern gerechnet; der Fehler
dieser Näherung bleibt weit unter der kleinsten zulässigen Toleranz.
Toleranz. Die Toleranz ist die grösste Abweichung, die ein entfernter Punkt vom verbleibenden Streckenverlauf haben darf. Sie hängt nicht am Algorithmus, sondern an den Benutzereinstellungen: Drei Stufen — grob, mittel, fein — mit je einem konfigurierbaren Meterwert zwischen 1,8 m (0,01 sm) und 1800 m (1 sm), erzwungen in der Ordnung grob ≥ mittel ≥ fein.
Beschränkung auf GETRACKT. Die Optimierung liest ausschliesslich Wegpunkte mit Status
GETRACKT. Geplante und beobachtete Wegpunkte gehen weder in die Berechnung ein noch werden sie
verändert oder neu nummeriert — sie tragen eigene, unabhängige Reihenfolgen (siehe AE-6 zur
unterschiedlichen Rolle der Ausprägungen).
Sperrpunkte. Vor dem Algorithmus wird die Punktfolge an ihren Sperrpunkten segmentiert: erster
Punkt, letzter Punkt und jeder Punkt, der über Position und Uhrzeit hinaus Daten trägt — Bild,
Manöver, Name, Notiz, Wetterangaben, Kompasskurs, Motor- oder Segelzustand. Vereinfacht wird nur
zwischen zwei Sperrpunkten. Nachträgliches Wiedereinfügen wäre die naheliegendere Umsetzung, würde
aber die Toleranzzusage der umgebenden Strecke verletzen; die Segmentierung hält sie ein. Nebenbei
kann so nie ein Bild verwaisen, weshalb der picture-service unbeteiligt bleibt.
Ausführung serverseitig und zweistufig. Der Endpunkt rechnet auf Wunsch nur (vorschau: true)
oder löscht in einer Transaktion und nummeriert die verbleibenden Punkte lückenlos neu. Das Löschen
vieler Wegpunkte muss atomar sein — eine clientseitige Schleife aus Einzellöschungen könnte
mittendrin abbrechen und einen inkonsistenten Track hinterlassen. Die Vorschau ist die einzige
Schutzmassnahme vor einem unumkehrbaren Löschvorgang; ein Undo gibt es bewusst nicht.
8.8 Testkonzept
| Stufe | Werkzeug | Umfang |
|---|---|---|
| Unit | Surefire, *Test.java |
Fachlogik ohne Infrastruktur, kein Docker |
| Integration | Failsafe, *IT.java, Testcontainers |
Controller gegen echte PostgreSQL bzw. echten Objektspeicher; JWT wird injiziert, Keycloak läuft nicht mit |
| HTTP | Bruno-Sammlung | Endpunktverhalten gegen die laufende Umgebung |
| End-to-End | Playwright | Anwendung im Browser gegen den vollständigen Compose-Verbund |
| Frontend-Unit | Vitest | Offline-Logik, Hilfsfunktionen |
Die Integrationstests verzichten bewusst auf Keycloak und injizieren das Token direkt. Das spart Startzeit und macht die Tests unabhängig von der Realm-Konfiguration; die Kehrseite ist, dass Fehler in der Realm-Einrichtung erst den End-to-End-Tests auffallen.
9. Architekturentscheidungen
AE-1 — Drei Backend-Dienste statt eines Monolithen
Entscheidung. Der Schnitt verläuft entlang der Ressourcenarten: Logbuchdaten, Binärdaten, Renderarbeit.
Begründung. Die drei haben grundverschiedene Profile. Logbuchdaten sind klein, transaktional und relational. Bilder sind gross, unveränderlich und gehören nicht in eine Datenbank. Das Rendern eines PDF ist rechenintensiv, dauert Sekunden und darf die Erfassung nicht ausbremsen. Ein Monolith müsste für alle drei zugleich ausgelegt sein.
Preis. Beim Löschen ist eine dienstübergreifende Abstimmung nötig, und der Export beschafft seine Daten über HTTP statt aus derselben Transaktion. Beides ist beherrschbar; der Fachschnitt bliebe im Monolithen ohnehin derselbe.
AE-2 — API-First mit generierten DTOs
Entscheidung. api/yacht-logbuch-api.yaml ist der Vertrag. Backend-DTOs und Frontend-Client werden
daraus generiert.
Begründung. Bei einem Entwickler, der zwischen Java und TypeScript wechselt, ist Vertragsdrift die wahrscheinlichste Fehlerquelle. Generierung macht sie unmöglich: Ein Feld, das in der Spezifikation fehlt, existiert auf beiden Seiten nicht.
Preis. Jede API-Änderung erfordert einen Generierungslauf, und die Mapper müssen nachgezogen werden.
AE-3 — Kaskadierende Löschung aufwärts
Entscheidung. Das Entfernen des letzten Kindes entfernt den Elternteil, bis hinauf zur Reise.
Implementiert im DeletionService, nicht über JPA-Annotationen.
Begründung. Ein Segeltag ohne Wegpunkte ist fachlich kein Segeltag, sondern ein Überbleibsel. Die Regel ist unüblich genug, dass sie an einer nachlesbaren Stelle stehen muss statt über Entity-Annotationen verstreut.
AE-4 — 404 statt 403 für fremde Daten
Entscheidung. Zugriffe auf fremde Reisen werden behandelt, als existierten sie nicht.
Begründung. Siehe 8.1 — die blosse Existenzbestätigung ist bei Bewegungsdaten bereits eine Preisgabe.
Preis. Die Fehlersuche wird unschärfer: „Nicht gefunden" kann zweierlei bedeuten.
AE-5 — Eine einzige Herkunft im Browser
Entscheidung. nginx liefert alles aus und proxied die Dienste unter /app/…. Keine
API-Subdomain.
Begründung. Es entfällt CORS vollständig — kein Preflight, keine Header-Pflege, keine Falle bei Anmeldeflüssen. Zudem genügt ein einziges Zertifikat.
Preis. nginx ist ein einzelner Punkt, an dem das Routing kaputtgehen kann, und die Konfiguration trägt entsprechend viel Verantwortung.
AE-6 — Getrackte Wegpunkte nicht im Logbuch-Export
Entscheidung. Der PDF- und HTML-Export blendet GETRACKT aus; der Excel-Rohdatenexport enthält
sie vollständig.
Begründung. Ein Tracking-Punkt alle paar Minuten ergibt über eine Reise Tausende Einträge. Als Logbuchtext sind sie unlesbar, als Strecke auf der Karte dagegen genau richtig. Der Rohdatenexport verfolgt das andere Ziel — Vollständigkeit — und schliesst sie deshalb ein.
AE-7 — Startseite ausserhalb der Anwendung
Entscheidung. Die öffentliche Startseite und diese Dokumentation sind statische Dateien, die zur Bauzeit entstehen, und keine Route in der React-Anwendung.
Begründung. Die Anwendung initialisiert vor dem ersten Bild ihre Anmeldeschicht. Eine Marketing- Seite darf das nicht: Sie muss sofort sichtbar, ohne Konto nutzbar und für Suchmaschinen lesbar sein. Als vier vorgerenderte Sprachfassungen erfüllt sie das ohne jedes JavaScript-Gerüst.
Preis. Zwei Darstellungswelten im selben Verzeichnisbaum, und Inhaltsänderungen berühren vier Sprachdateien. Ein Prüfschritt im Build bricht ab, wenn eine Übersetzung Schlüssel vermissen lässt.
AE-8 — Wiederkehrer-Weiterleitung serverseitig
Entscheidung. Ob ein Besucher die Startseite oder direkt die Anwendung sieht, entscheidet nginx anhand eines Cookies — nicht ein Skript auf der Seite.
Begründung. Eine Weiterleitung per JavaScript setzt voraus, dass die Seite erst geladen und gezeichnet wird. Genau dieses Aufblitzen soll Wiederkehrern erspart bleiben. Die Entscheidung fällt deshalb vor dem ersten ausgelieferten Byte.
Preis. Die Antwort auf / hängt von Cookie und Sprachheader ab und ist damit nicht cachebar. Bei
einem kleinen Dokument ist das verschmerzbar; alle übrigen Seiten und Assets bleiben voll cachebar.
10. Qualitätsanforderungen
| Qualitätsmerkmal | Szenario | Erwartetes Verhalten |
|---|---|---|
| Vertraulichkeit | Ein Anwender ruft die Reise eines anderen per ID ab | 404, kein Hinweis auf Existenz |
| Vertraulichkeit | Ein Anwender ohne logbuch-admin ruft den Excel-Export auf |
403, kein ZIP |
| Verfügbarkeit | Erfassung ohne Netz, Verbindung kehrt zurück | Alle Änderungen werden in Erfassungsreihenfolge nachgesendet, keine geht verloren |
| Datenhoheit | Export einer Reise ohne Törns und Segeltage | Dokument bzw. ZIP entsteht mit leeren Abschnitten, ohne Fehlschlag |
| Nachvollziehbarkeit | Reise enthält Wegpunkte aller drei Status | Logbuch-Export zeigt geplante und beobachtete, Rohdatenexport alle drei |
| Betreibbarkeit | Fehlerhafte Version in Produktion | Rollback über IMAGE_TAG in wenigen Minuten, ohne Datenmigration |
| Integrität | Schema weicht von den Entities ab | Der Dienst startet nicht, statt mit falschem Schema zu laufen |
11. Risiken und technische Schulden
| Risiko / Schuld | Auswirkung | Umgang |
|---|---|---|
| nginx als einzelner Routing-Punkt | Ein Fehler in der Reihenfolge der location-Blöcke kann Anwendung oder API unerreichbar machen |
Routing-Matrix wird vor jedem Deployment gegen den lokalen Container geprüft; End-to-End-Tests laufen über dieselben Regeln wie die Produktion |
| Reise-Logo im Dateisystem | Weicht vom Objektspeicher-Weg aller anderen Bilder ab; das Volume muss gesondert gesichert werden | Als Schuld vermerkt; Vereinheitlichung auf den picture-service wäre der saubere Weg |
| Keine Konfliktauflösung offline | Parallele Änderungen an zwei Geräten können einander überschreiben | Bewusst akzeptiert, siehe 8.2 |
| Verwaiste Bilder bei Löschfehlern | Blobs bleiben im Objektspeicher zurück | Harmlos gegenüber dem umgekehrten Fall; ein Aufräumlauf fehlt bislang |
| Keine Staging-Umgebung | Fehler zeigen sich erstmals in Produktion | Integrations- und End-to-End-Tests als Gate; Rollback über IMAGE_TAG ist schnell |
| Realm-Konfiguration nur end-to-end abgedeckt | Fehler in der Keycloak-Einrichtung fallen spät auf | Der Realm-Import ist Teil des Keycloak-Images und damit versioniert |
| Externer Kartendienst | Fällt er aus, fehlen Kartenausschnitte im Export | Der Export läuft weiter, nur ohne Karte |
12. Glossar
| Begriff | Bedeutung |
|---|---|
| Reise | Oberste Einheit des Logbuchs; klammert Törns, trägt Titel, Zeitraum, Schiff und Logo |
| Törn | Zusammenhängender Abschnitt innerhalb einer Reise; liegt in deren Zeitraum und überlappt keinen anderen Törn |
| Segeltag | Ein Tag auf dem Wasser mit Häfen, Distanz, Wetter, Segelzustand und Crew |
| Tag für andere Aktivitäten | Ein Tag der Reise ohne Fahrt — Hafentag, Wartung, Landausflug |
| Wegpunkt | Einzelner Punkt mit Koordinate, Zeit und Notiz |
GEPLANT |
Wegpunkt-Status: vor der Fahrt gesetzt, beabsichtigte Route |
GETRACKT |
Wegpunkt-Status: automatisch vom GPS aufgezeichnet; im Logbuch-Export ausgeblendet |
BEOBACHTET |
Wegpunkt-Status: von Hand gesetztes Ereignis |
| Aggregat | Im export-service die zusammengeführte Sicht auf eine Reise über alle Dienste hinweg |
| Zeitleiste | Verschmelzung von Segeltagen und Tagen für andere Aktivitäten zu einer chronologischen Folge |
besitzer_subject |
Der subject-Claim des Zugriffstokens; kennzeichnet den Eigentümer einer Reise |
| Sequentielle ID | Exportlokale fortlaufende Nummer je Entitätstyp im Excel-Export; ersetzt die interne UUID |
IMAGE_TAG |
Deployment-Variable in Dokploy; das Setzen auf einen Commit-SHA löst Pull und Neuerstellung aus |