SyncTicket: ein Local-First-Issue-Tracker mit Durable-Object-Relay
Local-First-Issue-Tracker in Echtzeit: Der Browser besitzt die Daten als Yjs-CRDT, ein Durable Object reicht Änderungen zwischen Browsern weiter, gesucht wird lokal. Ohne LLM. Live als Demo seit dem 22. Oktober 2026.
Ein Issue-Tracker, in dem der Browser die Daten besitzt, statt die eines Servers zu cachen: Änderungen funktionieren offline und verschmelzen ohne Konflikte, zwei Browser bleiben in Echtzeit synchron, alles für 0 EUR im Monat.
- Stack:
- Svelte 5 + SvelteKit 2 · Yjs + y-indexeddb · Cloudflare Durable Objects · FlexSearch · y-partyserver · Transformers.js (all-MiniLM-L6-v2) · virtua · Tailwind v4 + shadcn-svelte · Vitest + Playwright · Cloudflare Workers
- Zeitraum:
- 2026
- veröffentlicht:
Problem und Rahmenbedingungen
SyncTicket ist ein Issue-Tracker im Stil von Linear, gebaut, um die schweren Teile dieses Problems ernst zu nehmen: Der Browser besitzt die Daten, Änderungen funktionieren offline und verschmelzen ohne Konflikte, zwei Browser bleiben in Echtzeit synchron, und die Suche braucht keinen Server. Es ist ein Portfolio-Projekt, das zufällig eine echte Anwendung ist, kein Produkt auf der Suche nach Nutzern. Wer es öffnet, will es beurteilen; also muss sich jede Behauptung in der laufenden Demo oder im Quellcode prüfen lassen. Der Name ist ein Arbeitstitel.
Drei Rahmenbedingungen prägten es. Das Budget war 0 EUR, also läuft alles im Free Plan von Cloudflare, dessen Grenzen (100.000 Requests und 100.000 geschriebene Zeilen pro Tag für den ganzen Account und 2 MB pro gespeichertem Wert) zu Vorgaben für den Entwurf wurden statt zu Überraschungen. Wer die Demo besucht, sollte innerhalb einer Minute damit arbeiten können, also gibt es keine Konten: Der Workspace-Link ist der Zugang. Und die Demo muss weiterlaufen, ohne dass jemand sie beobachtet; wenn sie etwas nicht kann, muss sie es also sagen, statt stillschweigend zu scheitern.
Es nutzt kein LLM. Sein einziges Modell ist ein optionales Sentence-Embedding-Modell für die Suche, das im Browser der Besuchenden läuft.
Architektur
SyncTicket besteht aus zwei Workern im Free Plan von Cloudflare und ist seit dem 22. Oktober 2026 unter syncticket.elbaymalik.com live. Der App-Worker mit SvelteKit liefert die Seiten und das selbst gehostete Suchmodell aus; ein eigener Relay-Worker hält einen Durable-Object-Raum pro Workspace. Getrennt sind sie, weil der stabile SvelteKit-Adapter keine Durable-Object-Klasse exportieren kann.
Im Browser ist jeder Workspace ein Yjs-Dokument: eine Map von Issues, jedes Issue eine Map von Feldern. y-indexeddb speichert es in IndexedDB, und dieses Dokument, nicht der Server, ist die Quelle der Wahrheit. Eine Runes-Brücke in Svelte 5 beobachtet es und aktualisiert eine reaktive Projektion, die die Oberfläche liest; die Oberfläche schreibt nie in die Projektion, nur in das Dokument. Das Kanban-Board ordnet Karten über fraktionale Schlüssel mit zufälligem Suffix, damit zwei Browser, die offline eine Karte anlegen, nicht denselben Schlüssel erzeugen, und ein Reparaturschritt repariert gleiche Schlüssel, die schon existieren.
Das Relay, y-partyserver auf einem Durable Object mit Hibernation, reicht Updates zwischen Browsern weiter und speichert einen Snapshot des Dokuments im SQLite-Speicher des Raums, mit 2 s Debounce und höchstens 5 s Verzug. Die Hibernation sorgt dafür, dass ein ruhender Raum nichts kostet, und sie verdrängt den Speicher des Raums: Das Dokument muss bei jedem Aufwachen aus SQLite neu aufgebaut werden, und die Präsenzliste beginnt leer. Die Suche läuft in einem Modul-Web-Worker. FlexSearch antwortet ab dem ersten Tastendruck; schaltet jemand die Bedeutungssuche ein, läuft all-MiniLM-L6-v2 über Transformers.js auf WebAssembly, und beide Rankings werden mit Reciprocal Rank Fusion verschmolzen. Bei ausgeschalteter Bedeutungssuche zeichnet ein End-to-End-Test jede Anfrage auf, während Suchbegriffe getippt werden, und prüft, dass es keine gibt. Die Entscheidungen dahinter stehen im Entscheidungsprotokoll weiter unten, jeweils mit der Alternative, gegen die sie sich durchgesetzt haben.
Wo die Bibliotheken nicht reichten
Dreimal tat das installierte Paket nicht, was seine Dokumentation oder der Plan annahm. Nur die Präsenz war ein dokumentiertes Versprechen; die beiden anderen Lücken fand ich, indem ich den installierten Code las.
Präsenz: y-protocols dokumentiert, dass ein Peer, von dem 30 Sekunden lang nichts kommt, entfernt wird. y-partyserver löscht das Intervall, das dies tut, auf Client und Server, sodass ein Peer mit zugeklapptem Laptop für immer auf dem Bildschirm blieb. Die Hibernation machte es schlimmer, denn ein aufgewachter Raum hat eine leere Präsenzliste, und das Entfernen eines Peers wird an niemanden gesendet. Jetzt verantwortet der Client die Liveness, mit einem Heartbeat und einem Filter für veraltete Peers, so ausgelegt, dass ein Tab allein nichts sendet und den Raum ruhen lässt.
Verbindung: Der Provider schreibt die Zeit der letzten empfangenen Nachricht und liest sie nie, sodass ein Socket, der still wurde, ohne sich zu schließen, als verbunden und synchron galt. Der erste Entwurf einer Lösung, disconnect() und danach connect() aufzurufen, war doppelt kaputt, wie eine Probe gegen den installierten Provider vor der Auslieferung zeigte: connect() bewirkte nichts, und disconnect() meldete dauerhaft den Kanal ab, der Tabs desselben Browsers synchron hält. Die ausgelieferte Lösung schließt den verdächtigen Socket und lässt das Backoff der Bibliothek neu verbinden.
Speichern: Über der Zeilengrenze von 2 MB bei Cloudflare scheitert das Speichern des Relays mit einer Exception, und die Bibliothek schluckt den Fehler in eine Logzeile, die der Free Plan drei Tage aufbewahrt. Der Raum synchronisierte im Speicher weiter und hätte bei der nächsten Verdrängung alles seit dem letzten guten Speichern verloren. Jetzt prüft das Relay die Größe vor dem Schreiben, behält den letzten guten Snapshot, hält den Fehler dort fest, wo er die Hibernation übersteht, und sagt es jedem Client, auch denen, die später kommen.
Ergebnisse
Alle 13 Phasen der Roadmap sind erledigt, und beide Worker laufen seit dem 22. Oktober 2026 live im Free Plan. Das Deployment wurde so geprüft, wie es das Runbook des Repositorys verlangt, nicht am Ausbleiben einer Fehlermeldung: Das Verbindungs-Popover zeigte „Ready to share“, und ein Issue, das in einem zweiten Browser nur mit dem Share-Link angelegt wurde, erschien im ersten. Es ist eine dauerhafte, offene Demo für Beurteilende, bewusst ohne Konten: Wer einen Workspace-Link hat, kann den Workspace lesen und bearbeiten, und die App rät davon ab, echte Daten einzugeben.
Die Zahlen oben sind lokal gemessen. Die Virtualisierung brachte die Liste bei 10.000 Issues von 5.019 ms auf 33 ms; FlexSearch beantwortet einen ersten Tastendruck in 0,005 ms gegenüber 3,96 ms bei MiniSearch; und zwei Browser auf einem Rechner konvergierten über ein lokales Relay mit Medianen von 29 bis 30 ms, weit unter dem Ziel von 1 s, aber keine Zahl aus der Produktion. Ab rund 6.130 Issues wird ein Workspace nicht mehr gespeichert, und die App sagt das. Die Suchqualität ist nicht gemessen: Es gibt keinen Golden-Datensatz, das hier sind also Zahlen zur Geschwindigkeit, nicht zur Relevanz. Die Gates laufen lokal mit scripts/ci-local.sh, das den CI-Workflow Schritt für Schritt nachbildet.
Die offenen Punkte stehen im Repository. Der Name ist noch ein Arbeitstitel: Das Produkt bekam am 22. Oktober 2026 ein eigenes Logo, aber keinen Namen. Der Build liefert noch eine zweite, bytegleiche Kopie der 23,6 MB großen ONNX-WebAssembly-Datei mit, die nie ausgeliefert wird; beide zusammenzulegen, ist ein offener Folgeschritt. Stiller Verlust über das tägliche Zeilenbudget wird nur allgemein gemeldet, weil die Form seines Fehlers nicht geprüft ist. Eine halb offene Verbindung, die weder das Offline-Ereignis des Browsers noch die Rückkehr zum Tab aufdeckt, fällt erst beim TCP-Timeout des Browsers auf. Board-Cursor aus dem Plan wurden bewusst weggelassen, weil jede Zeigerbewegung einen ruhenden Raum wecken würde. Und es gibt noch keinen Export, um Daten aus einem Workspace zu holen, den das Relay nicht mehr speichern kann; die Daten liegen weiterhin in der eigenen IndexedDB der Besuchenden.
Lehren
Das installierte Paket lesen, nicht seine Dokumentation. Nur eine der drei Lücken oben brach ein dokumentiertes Versprechen; die beiden anderen waren Verhalten, das nur der installierte Code zeigte, und die erste Lösung für eine davon hätte eine stille Regression ausgeliefert. Eine Wegwerf-Probe gegen das Paket auf der Platte kostete weniger als jede von ihnen.
Erst messen, dann wählen. Bei 10.000 Issues kostete die reaktive Schicht 2 ms und das DOM 5 Sekunden, also war die Lösung eine Komponente statt eines Umbaus der Abfrageschicht; die Suchmaschine wurde anhand des ersten Tastendrucks gewählt, nicht anhand einer durchschnittlichen Abfrage.
Eine genannte Grenze schlägt eine verborgene. Der Free Plan setzt harte Grenzen, und wehgetan hätte die, die stillschweigend scheiterte. Den Nutzenden zu sagen, dass ein Workspace nicht mehr gespeichert wird, war billiger und ehrlicher als jedes Speicherschema, das die Wand weiter hinausschiebt.
Ein Dry Run ist kein Deployment. Der ADR zum Deployment-Ziel erwartete beim ersten echten Deployment kleine Überraschungen, und beide lagen im Werkzeug: wrangler login meldete einen Timeout, nachdem es gültige Zugangsdaten geschrieben hatte, und ein bloßes wrangler deploy warnte nur, dass die Relay-Konfiguration eine e2e-Umgebung ohne Rate Limiter enthielt. Beide Deploy-Skripte des Relays nennen jetzt ihre Umgebung.
Zentrale Entscheidungen
01 Ein Yjs-CRDT mit Durable-Object-Relay
Ein Yjs-Dokument pro Workspace als Quelle der Wahrheit, mit y-indexeddb in IndexedDB gespeichert und über ein Relay auf einem Cloudflare Durable Object mit y-partyserver synchronisiert; alle Abfragen laufen im Browser.
Offline-Schreibzugriffe und konfliktfreies Zusammenführen kommen aus dem CRDT, ohne ein eigenes Sync-Protokoll, das man falsch machen kann. Electric braucht einen ständig laufenden Dienst mit einem Replication Slot, was das Budget von 0 EUR sprengt; Zero unterstützt laut eigener Dokumentation keine Offline-Schreibzugriffe; Triplits Zukunft war nach der Übernahme ungewiss; PowerSync ist eine verwaltete Blackbox. y-durableobjects passte zum Entwurf, war aber seit rund 11 Monaten inaktiv, während Cloudflare y-partyserver pflegt.
ElectricSQL + PGlite blieb der dokumentierte Fallback, mit der Runes-Brücke aus Phase 3 als Punkt für einen Wechsel; die Brücke hielt (SyncTicket docs/adr/0001).
verworfen: ElectricSQL + PGlite · Rocicorp Zero · Triplit · PowerSync · y-durableobjects als Relay-Bibliothek
02 Keine Konten: Der Workspace-Link ist der Zugang
Keine Authentifizierung. Ein Workspace ist eine zufällige 128-Bit-ID in der URL, die als Bearer-Capability wirkt: Wer den Link hat, kann lesen und schreiben. Das Relay leitet nur Namen in der Form weiter, die die App erzeugt, und begrenzt WebSocket-Upgrades auf 30 pro 60 Sekunden und IP.
Die Demo besteht darin, einen Link in einem zweiten Browser zu öffnen und der Synchronisierung zuzusehen; ein Registrierungsschritt zerstört diesen Moment und bringt einen Session-Speicher ins Budget des Free Plans. Kurze IDs lassen sich durchprobieren, also muss die ID lang und zufällig bleiben.
Am Deployment gemessen: 60 gleichzeitige Upgrade-Versuche von einer Adresse ergaben 42 Verbindungen, das Limit ist also eine Größenordnung, keine exakte Anzahl. Der ADR behält seine eigene Korrektur, durchgestrichen und datiert, zu einer Missbrauchsabwehr, die er einmal behauptete und die nie gebaut worden war. Es ist eine dauerhafte Demo, Konten sind nicht geplant (SyncTicket docs/adr/0004, PRODUCT.md).
verworfen: Konten mit Better Auth von Anfang an · Anonyme Geräte-IDs mit Allowlists · Kurze, lesbare Workspace-IDs
03 virtua für die Liste, eine Obergrenze für das Board
Der Virtualizer von virtua rendert die Issue-Liste als echte Listeneinträge in einer echten Liste; das Kanban-Board wird nicht virtualisiert, sondern auf 100 Karten pro Spalte begrenzt, und ein Drop wird trotzdem relativ zu den Karten jenseits der Grenze einsortiert.
Zuerst gemessen: Filtern und Sortieren von 10.000 Issues dauerte 2 ms, ihr Rendern 5.019 ms, das Problem war also allein das DOM. svelte-virtuallists war seit 18 Monaten nicht veröffentlicht worden und rendert nur divs; der Svelte-Adapter von TanStack liefert einen Legacy-Store, den die Runes-only-Regel des Projekts verbietet; svelte-tiny-virtual-list braucht eine feste Höhe in Pixeln. Die Drag-Bibliothek indiziert die direkten Kinder einer Spalte, Virtualisierung und Drag-and-drop schließen sich also aus.
Rendern der Liste 5.019 ms → 33 ms; eine Spalte mit 2.028 Karten 907 ms → 194 ms mit Obergrenze (SyncTicket docs/adr/0005).
verworfen: svelte-virtuallists (der Pin der Roadmap) · @tanstack/svelte-virtual · svelte-tiny-virtual-list · Auch das Board virtualisieren · Alles rendern und die Zeile verschlanken
04 FlexSearch in einem Worker, mit optionalem Embedding-Modell
FlexSearch für die lexikalische Suche in einem Modul-Web-Worker; ein Embedding-Modell (all-MiniLM-L6-v2 über Transformers.js) lädt erst, wenn jemand es anfordert, und beide Rankings werden mit Reciprocal Rank Fusion verschmolzen. Modell und ONNX-Runtime kommen vom eigenen Origin der App, entfernte Modelle sind abgeschaltet.
Der erste Tastendruck ist ein einzelner Buchstabe, der fast alles trifft, und genau dort brauchte MiniSearch bei 10.000 Issues 3,96 ms gegenüber 0,005 ms bei FlexSearch, ein Abstand, der mit dem Korpus wächst, obwohl MiniSearch die bessere API hat und dreizehnmal schneller aktualisiert. Orama setzt voraus, dass es beide Indizes besitzt, und das Ranking sollte explizit und testbar sein. Einen Modell-Download von 23 MB beim ersten Besuch aufzuzwingen, kam nicht infrage.
Durch das Selbsthosting (ADR 0009) hängt die semantische Suche zur Demozeit von keinem Dritten ab; ein End-to-End-Test prüft, dass keine Anfrage Hugging Face oder jsDelivr erreicht, und fehlt eine lokale Datei, fällt die Suche auf lexikalisch zurück, statt ins Netz zu gehen (SyncTicket docs/adr/0006, docs/adr/0009).
verworfen: MiniSearch · Orama · Der vorhandene Substring-Filter · Semantische Suche standardmäßig an · Das Modell von Hugging Face und die Runtime von jsDelivr laden (der erste Entwurf, durch ADR 0009 ersetzt)
05 Präsenz-Liveness in der Verantwortung des Clients
Jeder Client sendet alle 10 Sekunden einen Heartbeat, solange er andere kennt, blendet einen Peer aus, von dem er 25 Sekunden nichts gehört hat, und vergisst ihn nach 2 Minuten; onError im Relay räumt genauso auf wie onClose.
Der dokumentierte 30-Sekunden-Sweep läuft nie: y-partyserver löscht sein Intervall auf Client und Server, sodass ein Peer, der ohne Schließen seines Sockets verschwand, für immer sichtbar blieb, und die Hibernation leert zudem die Präsenzliste des Raums. Aufräumen per Broadcast ließe die schlechte Verbindung eines Clients einen gesunden Peer von jedem Bildschirm werfen, und ein bedingungsloser Heartbeat würde einen ruhenden Raum rund 8.600-mal am Tag wecken.
Die Verbindung hatte dieselbe Art Lücke: Der Provider notiert, wann er zuletzt vom Server gehört hat, und liest es nie, sodass ein halb offener Socket für immer verbunden aussah. Die Lösung schließt einen verdächtigen Socket und lässt das Backoff der Bibliothek neu verbinden; sie nutzt nie disconnect() mit anschließendem connect(); eine Probe zeigte, dass das nicht neu verbindet und die Synchronisierung zwischen Tabs dauerhaft abschaltet (SyncTicket docs/adr/0007, docs/adr/0008).
verworfen: Dem 30-Sekunden-Timeout des Protokolls vertrauen · Den Sweep des Protokolls im Client wiederherstellen · Mit removeAwarenessStates aufräumen, das an alle sendet · Ein Heartbeat, der nie aufhört
06 Die Snapshot-Grenze melden, nicht anheben
Das Relay prüft die Größe jedes Snapshots vor dem Schreiben gegen eine Schwelle von 1,9 MB, behält den letzten guten Snapshot, statt einen Schreibversuch zu wagen, der scheitern würde, hält den Fehler in einer Statuszeile fest, die die Hibernation übersteht, und sagt es jedem Client, auch denen, die später verbinden. Gespeichert wird mit 2 s Debounce und höchstens 5 s Verzug.
Über der Zeilengrenze von 2 MB bei Cloudflare löst der Schreibzugriff eine Exception aus, und y-partyserver schluckt den Fehler in eine Logzeile: Der Raum synchronisiert im Speicher weiter und verliert bei der nächsten Verdrängung alles seit dem letzten guten Speichern. Verteilen auf Zeilen tauscht diesen vorhersagbaren Fehler gegen einen sporadischen im täglichen Zeilenbudget; V2 bringt 13–37 % Spielraum, aber ein V2-Payload, als V1 angewendet, endet stillschweigend mit einem leeren Dokument; Kompaktierung erzeugt neue Client-IDs, und das nächste Zusammenführen holt zurück, was sie verworfen hat.
Das alte Debounce von 300 ms konnte bis zu rund 288.000 Zeilenschreibvorgänge pro Tag aus einem Raum erreichen, gegen ein Budget von 100.000 für den ganzen Account; das neue Timing begrenzt den stoßweisen Fall auf rund 43.200. Der Weg zum selben Fehler über das tägliche Zeilenbudget wird weiterhin nur allgemein gemeldet (SyncTicket docs/adr/0010).
verworfen: Den Snapshot auf mehrere Zeilen verteilen · Auf das V2-Update-Format von Yjs wechseln · Kompaktierung · Ein Append-only-Log der Deltas · Ab der Grenze Schreibzugriffe ablehnen oder Sockets schließen
Evaluationsergebnisse
| Kennzahl | Wert | Status |
|---|---|---|
| Rendern der Liste bei 10.000 Issues, vor → nach der Virtualisierung | 5.019 ms → 33 ms | gemessen DOM-Knoten 168.011 → 383, Listeneinträge 10.000 → 20. Filtern und Sortieren aller 10.000 Issues dauerte vor der Änderung 2,0 ms: Der Engpass war das DOM, nicht die reaktive Schicht. |
| Rendern einer Board-Spalte, alle Karten → auf 100 begrenzt | 907 ms → 194 ms | gemessen Eine Spalte hielt 2.028 Karten: 38.979 DOM-Knoten vor der Begrenzung, 1.922 danach. Das Board lässt sich nicht virtualisieren, weil die Drag-Bibliothek jede Karte im DOM braucht. |
| Abfrage beim ersten Tastendruck bei 10.000 Issues, MiniSearch → FlexSearch | 3,96 ms → 0,005 ms | gemessen Der Preis: FlexSearch baut seinen Index in 101 ms statt 49 ms und aktualisiert ein Dokument in 1,3 ms statt 0,10 ms. |
| Bundle des Such-Workers, Vites Standardformat für Worker → ES-Module | 500 KB → 51 KB | gemessen Das Standardformat bettet dynamische Importe ein; die Embedding-Bibliothek hätte damit jeden Besuch erreicht, ob eingeschaltet oder nicht. |
| Konvergenz zweier Browser, Median aus fünf Round Trips, drei Läufe | 29 / 30 / 30 ms | gemessen Gegen ein lokales Relay auf demselben Rechner, kein Wert über 36 ms; das Abnahmekriterium war unter 1 s. Keine Zahl aus der Produktion: Der Weg übers Internet zwischen zwei echten Browsern steckt nicht darin. |
| Größter Workspace, den das Relay noch speichert | ~6.130 Issues | gemessen Rund 310 kodierte Bytes pro generiertem Issue bei 6.000 Issues, gegen eine Schwelle von 1,9 MB unter der Zeilengrenze von 2 MB bei Cloudflare. Längere Beschreibungen senken den Wert: ADR 0010 maß 2.760 Issues an der 2-MB-Grenze bei Beschreibungen von rund 400 Zeichen. |
Gemessen: SyncTicket README.md, Phase 6, und docs/adr/0005 (lokal in Chromium gemessen) · SyncTicket docs/adr/0005 (lokal in Chromium bei 10.000 Issues gemessen) · SyncTicket README.md, Phase 8, und docs/adr/0006 (lokal in Node an 10.000 generierten Issues gemessen) · SyncTicket README.md, Phase 8 · SyncTicket README.md, Phase 9 (in Phase 13 neu gemessen, docs/ROADMAP.md) · SyncTicket PRODUCT.md, docs/adr/0010 und src/lib/sync/snapshot-capacity.test.ts
Kosten & Latenz
| Kennzahl | Wert | Status |
|---|---|---|
| Monatliche Betriebskosten | 0 EUR/Monat | gemessen Die Grenzen des Free Plans sind die Grenzen des Produkts: 100.000 Requests und 100.000 geschriebene Zeilen pro Tag, für den ganzen Account. |
| Upload beim Deployment, App-Worker / Relay-Worker | 1.579,82 KiB / 333,83 KiB | gemessen Dazu 42 statische Assets; das größte ist die ONNX-Runtime mit 23,6 MB, unter der Grenze von 25 MiB pro Datei bei Cloudflare. |
Gemessen: SyncTicket PRODUCT.md, Positioning, und docs/DEPLOY.md (beide Worker im Free Plan von Cloudflare) · SyncTicket docs/DEPLOY.md, The deploy that exists (22. Oktober 2026)
Sicherheit & Fehlerfälle
Risiken
- Wer einen Workspace-Link hat, kann den Workspace lesen und bearbeiten und den Link weitergeben
- Eine Flut von Anfragen kann das kostenlose Tagesbudget des Accounts aufbrauchen und beide Worker bis 00:00 UTC offline nehmen
- Ein Workspace über der Zeilengrenze von 2 MB oder ein Tag über 100.000 Zeilenschreibvorgängen wird nicht mehr gespeichert, scheint aber weiter zu synchronisieren
- Anzeigename und Farbe eines Peers stammen von jedem, der den Link hat
Gegenmaßnahmen
- Eine zufällige 128-Bit-ID, die sich nicht durch Ausprobieren finden lässt, bewusst keine Konten und eine Demo, die davon abrät, echte Daten einzugeben
- Eine Prüfung der Raumnamen, bevor ein Durable Object entsteht, und 30 WebSocket-Upgrades pro 60 Sekunden und IP, in der Produktion als großzügig gemessen (42 von 60 gleichzeitigen Versuchen verbanden sich): Das macht aus einer Flut ein Rinnsal und garantiert nichts
- Die Größe wird vor jedem Speichern geprüft, der letzte gute Snapshot bleibt erhalten, eine Statuszeile übersteht die Hibernation und jeder Client wird informiert; der Weg über das Zeilenbudget wird allgemein gemeldet
- Namen werden von Steuer- und Bidi-Zeichen befreit und in der Länge begrenzt; eine Farbe reist als Farbton-Zahl, die über oklch() gerendert wird, nie als String