Wie der Sync arbeitet

Delta-Sync über eine Sequenznummer, Konfliktauflösung über eine Hybrid Logical Clock, und eine Warteschlange, die einen Absturz überlebt. Der Server ist dabei ein Briefkasten, kein Gehirn.

Ohne Server

Der Abgleich ist freiwillig. Beim ersten Start bietet die Einrichtung neben Konto, Kopplung und Wiederherstellung einen vierten Weg an: Ohne Server nutzen. Danach ist tessera vollständig bedienbar — Hosts, Gruppen, Snippets, Schlüssel, Sprunghosts, Dateien, alles. Nur bleibt es auf diesem Gerät.

Was in diesem Zustand nicht existiert:

In der Fußzeile der Hostliste steht statt des Abgleichstands Nur auf diesem Gerät, und daneben der Weg heraus. Denselben Weg gibt es im Menü und in den Einstellungen unter Abgleich.

Nachrüsten geht jederzeit und verliert nichts. Jeder Datensatz trägt von Anfang an seine Uhr und sein dirty-Kennzeichen — auch ohne Server. Kommt später ein Konto dazu, ist der gesammelte Bestand genau das, was die Push-Warteschlange ohnehin enthält, und geht beim ersten Durchlauf verschlüsselt hoch. Für den Weg über eine Kopplung gilt dasselbe: die lokalen Datensätze kommen zu denen des Kontos hinzu, ihre IDs sind zufällig und kollidieren nicht.

Die Knoten-ID der Uhr wird beim serverlosen Start einmal vergeben und bleibt danach dieselbe — auch über den Wechsel zu einem Konto hinweg bleiben die damit gestempelten Datensätze einsortierbar.

Umgekehrt gilt das nicht: Ein Gerät, das bereits an einem Konto hängt, kann nicht in den serverlosen Betrieb zurück. Es bekäme sonst einen Bestand, den niemand mehr abgleicht, während der Vault weiterläuft. Deshalb bietet die Einrichtung diesen Weg nur an, solange noch keine Geräteidentität existiert.

Start ohne Serverkontakt

Die Oberfläche wartet beim Start nicht auf den Server. Der entschlüsselte Arbeitsstand liegt in der lokalen Datenbank — Hostliste, Snippets, Schlüsselbund, gespeicherte Benutzer —, und er steht bereit, bevor das erste Byte über die Leitung ging. Die Sitzung zum Server wird nachgeholt, während die App schon bedient wird. Ein eingerichtetes Gerät landet deshalb auch bei totem Server nie wieder auf dem Einrichtungsbildschirm.

Was ohne Sitzung nicht geht — für die Sekunde beim Start ebenso wie dauerhaft, solange niemand antwortet:

In der Fußzeile der Hostliste steht dazu erst Verbindet … und danach entweder der Abgleichstand oder Offline -- kein Serverkontakt; daneben liegt Erneut verbinden. Verbinden lässt sich in beiden Zuständen: Ein SSH- oder RDP-Ziel geht die App direkt an, der eigene Server hat damit nichts zu tun.

Der Entzug bleibt der Entzug. Meldet der Server beim Nachholen der Sitzung, dass dieses Gerät entzogen wurde oder der Vault Key rotiert ist, schließt tessera alle offenen Sitzungen und zeigt den Sperrhinweis — auch wenn die Oberfläche einen Augenblick vorher schon stand.

Was ein Datensatz ist

Hosts, Gruppen, Snippets, Schlüssel, gespeicherte Benutzer und Einstellungen sind je ein Record. Jeder trägt eine ID, einen Typ, eine Uhr und ein Löschkennzeichen. Der Klartext ist kanonisches JSON in UTF-8 — bewusst ohne Kompression, weil die Größe eines Records sonst Rückschlüsse auf den Inhalt erlaubte.

Gelöscht wird über Grabsteine: Der Record bleibt bestehen und ist als gelöscht markiert. Ein einfaches Verschwinden wäre von „habe ich noch nie gesehen“ nicht unterscheidbar, und das andere Gerät schöbe den Eintrag wieder hoch.

Ein Durchlauf

Erst hoch, dann runter.

Hoch
1. Alle Records mit dirty = true einsammeln
2. In Batches teilen (höchstens 200 Records oder rund 1,2 MB)
3. Je Batch mit dem Schlüssel der aktuellen Epoch verschlüsseln, hochladen
4. applied → dirty = false, Sequenznummer merken
   stale   → dirty bleibt; der Pull holt gleich den neueren Stand
Runter
1. Delta ab dem gespeicherten Cursor holen
2. Fremde Epoch? → Vault Key neu holen, Seite wiederholen
3. Je Record:
     Typ unbekannt          → überspringen, Cursor trotzdem weiter
     nicht entschlüsselbar  → überspringen, Fehler merken, weitermachen
     lokale Uhr ≥ fremde    → verwerfen, der lokale Stand gewinnt
     sonst                  → schreiben, dirty = false, Uhr mergen
4. Cursor speichern, bei Bedarf nächste Seite

Die Uhr

Eine Hybrid Logical Clock verbindet Zeitstempel mit einem Zähler und einer Knoten-ID:

Aufbau einer HLC
000001a2b3c4d5e6-0007-dev2f1c9a
└ 12 Hex: Millisekunden ┘ └ Zähler ┘ └ Knoten ┘

Feste Feldbreite ist der ganze Trick: Damit ist der lexikografische Vergleich identisch zum zeitlichen. Ohne Nullauffüllung wäre "10" < "9". Die Knoten-ID enthält keinen Bindestrich, sonst wäre die Zerlegung mehrdeutig.

Nach jedem übernommenen Record wird die lokale Uhr mit der eingehenden gemerged. Danach ist die eigene Uhr echt größer als alles Gesehene.

Was die Engine zusagt

Konflikte

Aufgelöst wird mit Last-Write-Wins über die Uhr — und zwar auf beiden Seiten. Der Client entscheidet lokal, der Server weist einen Push zurück, dessen Uhr nicht echt größer ist als der gespeicherte Stand. Beide Seiten unabhängig: Deshalb kann ein Fehler auf einer Seite den Stand nicht still verfälschen.

Push-Hinweise

Über eine WebSocket-Verbindung meldet der Server, dass es etwas Neues gibt — mehr nicht. Nutzdaten gehen nie über diesen Kanal; der Client holt sie per Delta-Abruf nach. Fällt der Socket aus, ist Polling im Minutentakt der vorgesehene Rückfall.

Authentifiziert wird über den Header des Upgrade-Requests, nicht über einen Query-Parameter, der im Zugriffslog landen würde. Der Socket ist bewusst kein Schreibkanal: Alles außer einem Ping wird ignoriert.