P2P Sync Spezifikation
Zuletzt aktualisiert: 11.10.2026
Globale Dezentrale Sync-Spezifikation (P2P & Async)
Serverlose, offline-fähige Datensynchronisation für mobile Erfassungs-Apps
1. Einleitung & Grundprinzipien
Diese Spezifikation beschreibt ein universelles, serverloses Synchronisationsprotokoll für mobile Anwendungen zur Vor-Ort-Datenerfassung. Das System funktioniert ohne zentralen Server, ohne Cloud-Dienst und ohne permanente Internetverbindung.
1.1. Kernanforderungen
- Zero-Infrastructure: Keine Serverkosten, keine Hosting-Wartung, keine Angriffsfläche für serverbasierte Datenlecks.
- Offline-First & Dezentral: Jedes Gerät ist autonomer Knotenpunkt (Peer) und verwaltet eine lokale Datenbank (z. B. SQLite / Drift).
- Zwei Transport-Modi:
- P2P (Echtzeit / Synchron): Direktes Geräte-Handshake im Feld (Hotspot, lokales WLAN, Bluetooth).
- Async (Einweg / Asynchron): Paket-Austausch über Messenger (WhatsApp, Signal), E-Mail oder Dateisystem.
- Absolute Idempotenz & Zombie-Schutz: Beliebig häufiges Importieren derselben Datei führt niemals zu Duplikaten oder Datenverlust.
2. Generisches Daten- und Entitätenmodell
Um das Protokoll für beliebige Fachanwendungen wiederverwendbar zu machen, ist das Modell in drei abstrakte Schichten unterteilt:
classDiagram
class Entity {
+UUID id
+Integer version
+DateTime updated_at
+DateTime deleted_at
+GeoPoint coordinates
+Map properties
}
class ObservationEvent {
+UUID id
+UUID parent_entity_id
+DateTime event_timestamp
+String author_signature
+Map observations
}
class MediaAttachment {
+SHA256 hash
+String mime_type
+Integer byte_size
+UUID parent_ref_id
}
class EntityAlias {
+UUID alias_id
+UUID canonical_id
+DateTime merged_at
}
Entity "1" <|-- "0..*" ObservationEvent : besitzt
ObservationEvent "0..1" <|-- "0..*" MediaAttachment : verknüpft mit
Entity "1" <|-- "0..*" EntityAlias : weitergeleitet von
2.1. Entitäten (Entities) – Zustandsobjekte
- Repräsentieren reale, ortsfeste Objekte vor Ort (z. B. Stationen, Messpunkte, Geometrien).
- Primärschlüssel ist eine clientseitig erzeugte UUIDv7 (oder UUIDv4).
- Besitzen eine monotone
version(Start: 1, Inkrement bei jeder Änderung) undupdated_at. - Löschungen erfolgen über Soft Deletes (
deleted_at: ISO8601).
2.2. Beobachtungsereignisse (Events / Observations) – Append-Only
- Zeitreihendaten, Kontrollen, Messungen, Inspektionen.
- Konfliktfrei: Jedes Ereignis ist ein eigener Datensatz mit eigener UUID. Mehrere Erfasser überschreiben sich nicht, sondern erweitern die Historie.
2.3. Medienanhänge (Media / Attachments) – Content-Addressed
- Fotos, Audiodateien, PDF-Messprotokolle.
- Primärschlüssel im Speicher ist die SHA-256 Prüfsumme des Datei-Inhalts.
- Ein Bild wird dateisystemseitig nur ein einziges Mal vorgehalten, egal wie viele Datensätze darauf verweisen.
2.4. Fremd-UUIDs & Aliase (Aliases / merged_into)
- Speichert Zusammenführungen von räumlichen Duplikaten (
alias_id→canonical_id). - Verhindert das Wiederauferstehen gelöschter/zusammengeführter Objekte („Zombie-Duplikate“).
3. Die zwei Transport-Modi
flowchart TD
subgraph SubAsync ["Hauptkanal: Asynchron via Chat & E-Mail (Standard 95% der Fälle)"]
A1["Erfasser: Einzelfund oder Tagestour"] --> A2["Kompaktes .discovery / .sync Paket erzeugt"]
A2 --> A3["System Share-Sheet: WhatsApp / Signal / E-Mail"]
A3 --> A4["Empfänger tippt Datei im Chat an"]
A4 --> A5["App öffnet sich: 1-Weg-Deduplizierung & Triage auf Karte"]
end
subgraph SubP2P ["Fallback-Kanal: P2P Echtzeit (Nur bei gemeinsamem Treffen im Funkloch)"]
B1["Beide Geräte im selben lokalen Hotspot"] --> B2["Have/Want Handshake via QR-Code"]
B2 --> B3["Gerät B fordert nur fehlende Daten an"]
B3 --> B4["Gerät A überträgt Payload lokal"]
end
3.1. Primärer Hauptkanal: Asynchroner Transport (Chat, Messenger, E-Mail)
In der Praxis ist Messenger- und E-Mail-Austausch der absolute Standard (> 95 % aller Fälle): Ein Ranger oder Ehrenamtlicher erfasst Daten und schickt sie abends oder direkt aus dem Wald per WhatsApp, Signal oder Mail an den Koordinator oder in die Fachgruppe.
1. Die Ein-Klick-Erfahrung für den Absender
- Einzelfund teilen: Klick auf „Teilen“ → App erzeugt
fund_20261011_1430.discovery→ WhatsApp-Kontakt auswählen → Gesendet. - Tagestour teilen: Klick auf „Heutige Tour teilen“ → App packt nur die Neuerungen des Tages in eine kleine
.sync-Datei → Per Mail an Koordinator → Gesendet. - Kein Vorwissen, keine IP-Adressen, kein technischer Aufwand.
2. Empfänger-seitige Verarbeitung (Robuste 1-Weg-Deduplizierung)
Beim Antippen der Datei in WhatsApp oder im E-Mail-Anhang importiert die App sofort autonom:
- Entität existiert bereits:
- Wenn
incoming.version <= local.version: Datensatz wird ignoriert (schützt vor doppelten Mails/Weiterleitungen). - Wenn
incoming.version > local.version: Lokaler Datensatz wird aktualisiert.
- Wenn
- Medien-Dateien:
- Berechne SHA-256. Existiert die Datei bereits im lokalen Medien-Ordner, wird sie nicht erneut entpackt/kopiert.
- Aliase prüfen:
- Befindet sich die eingehende ID bereits in der lokalen
aliases-Tabelle, wird sie direkt auf diecanonical_idumgeschrieben.
- Befindet sich die eingehende ID bereits in der lokalen
3. Praxisfalle: Warum Messenger-Dateien nicht im Download-Ordner landen (Android Scoped Storage)
Unter Android (ab Android 10/11) speichert WhatsApp empfangene Dokumente in einem isolierten, anwendungsspezifischen Medienverzeichnis:
/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Documents/
- Das Problem: Android blockiert im Standard-Dateimanager den Zugriff auf Unterordner von
/Android/media/. Für normale Nutzer ist es im manuellen Datei-Picker daher fast unmöglich, die Datei wiederzufinden. - Architektur-Lösung der Spezifikation (Die 3 Wege für Apps):
- Gold-Standard (Zero-Click Intent-Filter):
- Die Client-App registriert im
AndroidManifest.xmlIntent-Filter fürACTION_VIEWundACTION_SEND(MIME-Types für ZIP/Octet-Stream und Extensions.sync/.discovery). - Der Nutzer tippt im Chat einfach auf die Datei → Android öffnet die App direkt → App streamt das Paket aus dem Android
content://-URI ohne manuellen Dateimanager.
- Die Client-App registriert im
- System Share-Sheet („Teilen mit…“):
- In WhatsApp lange auf die Datei tippen → Drei-Punkte-Menü → „Teilen“ → App auswählen.
- Manueller Download-Fallback:
- Will der Nutzer zwingend den normalen Datei-Picker nutzen: In WhatsApp auf die Datei tippen → „Speichern unter…“ (nicht nur „Speichern“) wählen und oben gezielt den Ordner „Downloads“ auswählen.
- Gold-Standard (Zero-Click Intent-Filter):
3.2. Der P2P-Direktabgleich: Einseitiger Transfer (Sender → Empfänger)
Der direkte P2P-Abgleich im Feld folgt einem klaren, kontrollierten Rollenmodell zwischen Sender und Empfänger:
- Sender: Sucht aus, was bereitgestellt werden soll (z. B. Gesamtdatenbestand oder ein gefilterter Scope), startet den lokalen Server und zeigt den QR-Code auf seinem Display an.
- Empfänger: Scannt den QR-Code mit der Kamera, lädt das Datenpaket herunter und prüft eigenständig, was davon lokal gebraucht wird.
- Strikt einseitig (Unidirektional): Es findet keine automatische Rückübertragung vom Empfänger an den Sender statt. Möchte der Partner umgekehrt ebenfalls Daten weitergeben, wird die Rolle gewechselt (neuer Scan in umgekehrter Richtung).
sequenceDiagram
autonumber
actor A as Sender (Gerät A)
actor B as Empfänger (Gerät B)
Note over A: 1. Sender wählt Daten/Scope aus
Note over A: 2. Startet lokalen HTTP-Server & generiert Einmal-Token
A->>B: Zeigt P2P-QR-Code auf Display (http://ip:port/sync?token=xyz)
Note over B: 3. Empfänger scannt QR-Code mit Kamera
B->>A: GET /sync?token=xyz
A->>B: Sendet ZIP-Paket (data.json + Belegfotos)
Note over A: Übertragung abgeschlossen -> Server beendet
rect rgb(240, 248, 255)
Note over B: 4. Empfänger prüft & dedupliziert:
Note over B: - Version > Lokal? -> Aktualisieren
Note over B: - Version <= Lokal? -> Ignorieren
Note over B: - Foto SHA-256 vorhanden? -> Nicht neu schreiben
Note over B: - ID in aliases? -> Canonical ID nutzen
end
Note over B: Import fertig: Nur benötigte Daten übernommen!
Die entscheidenden Vorteile des einseitigen P2P-Syncs:
- Volle Kontrolle beim Sender:
- Der Sender entscheidet bewusst, wann und welche Daten bereitgestellt werden.
- Eigenständige Bedarfsprüfung beim Empfänger:
- Der Empfänger überschreibt keine neueren lokalen Stände und importiert nur das, was tatsächlich fehlt oder neuer ist.
- Keine unerwünschte Datenvermischung:
- Da der Empfänger nicht automatisch ungefragt Daten zurücksendet, bleibt der Sender-Datenbestand unberührt.
- Keine WhatsApp-Bildkompression (Volle Bildauflösung):
- P2P überträgt die originalen Belegfotos verlustfrei direkt von Gerät zu Gerät.
- Gigantische Übertragungsgeschwindigkeit:
- Lokale Wi-Fi-Verbindungen übertragen Pakete mit bis zu 30–60 MB/s in Sekundenschnelle – völlig ohne Internet oder Mobilfunk.
3.3. Scoping: Zeitliche und räumliche Einschränkung der Übertragung
Weder beim P2P-Kollegen-Sync noch beim Export per Mail/Chat soll zwingend der historische Gesamtbestand des gesamten Bundeslands übertragen werden. Oft ist der Kollege nur für ein bestimmtes Waldrevier zuständig oder benötigt nur die Daten der aktuellen Kontrollsaison.
Daher unterstützt das Protokoll präzise räumliche und zeitliche Filter:
flowchart TD
All[(Lokale Datenbank 5.000 Objekte)] --> Filter{Scope-Filter aktiv?}
Filter -- Räumlich --> S1[Umkreis z.B. 15km um GPS-Standort]
Filter -- Räumlich --> S2[Revier / Projekt z.B. Forstbezirk Ost]
Filter -- Räumlich --> S3[Bounding Box z.B. Aktueller Kartenausschnitt]
Filter -- Zeitlich --> T1[Letzte 24h / Heutige Tour]
Filter -- Zeitlich --> T2[Aktuelle Saison / Letzte 30 Tage]
Filter -- Zeitlich --> T3[Seit Zeitstempel X]
S1 & S2 & S3 & T1 & T2 & T3 --> Stream[Gefiltertes Manifest & Delta-Paket 25 Objekte]
1. Räumliche Einschränkung (Spatial Scope)
- Radius-Filter (Standard im Feld):
- „Nur Objekte im Umkreis von [10 / 25 / 50] km um meinen aktuellen Standort synchronisieren.“
- Die App berechnet den Abstand via Haversine-Formel und filtert vor dem Manifest-Austausch.
- Revier- / Projekt-Filter (
project_id):- „Nur Objekte des Reviers ‘Forstbezirk_Nord’ übertragen.“
- Schützt Reviergrenzen und verhindert das Übertragen fremder Zuständigkeitsbereiche.
- Bounding Box (Kartenausschnitt):
- Synchronisiert exakt das geografische Rechteck (
min_lat,min_lon,max_lat,max_lon), das der Nutzer gerade auf der Karte sieht.
- Synchronisiert exakt das geografische Rechteck (
2. Zeitliche Einschränkung (Temporal Scope)
- Tour-Filter:
- „Nur Neuerungen der letzten 24 Stunden / heutigen Tour.“
- Saison- / Intervall-Filter:
- „Nur Kontrollbesuche der letzten 30 Tage bzw. des laufenden Jahres.“
- Alte historische Besuche der Vorjahre bleiben unberührt.
- Timestamp-Cursor:
- „Alle Änderungen seit
2026-10-01T00:00:00Z.“
- „Alle Änderungen seit
3. Protokoll-Implementierung im Handshake & Manifest
Der Filter wird beim P2P-Handshake direkt im JSON ausgehandelt:
{
"sync_action": "handshake",
"scope": {
"temporal": {
"mode": "since",
"since_timestamp": "2026-10-01T00:00:00Z"
},
"spatial": {
"mode": "radius",
"center": { "latitude": 48.45210, "longitude": 8.31240 },
"radius_km": 15.0
}
}
}
- Ergebnis: Beide Geräte tauschen nur das aus, was für den gemeinsamen Arbeitstag und das aktuelle Gebiet relevant ist. Der Sync bleibt extrem schnell, ressourcenschonend und datenschutzkonform.
4. Einzel-Meldungen & Ad-Hoc Entdeckungen (Single Discovery Sharing)
In der Praxis werden neue Entdeckungen häufig einzeln und ad hoc gemeldet – beispielsweise wenn ein Spaziergänger, ehrenamtlicher Helfer oder Kollege unterwegs spontan auf ein neues Objekt stößt und diesen Fund sofort teilen möchte.
4.1. Was beinhaltet eine Einzel-Meldung?
Ein Einzelfund-Paket ist die kleinste atomare Einheit des Datenmodells:
- Genau 1 Entität: GPS-Position, Typ, vorläufige Attribute.
- 1 Erstbeobachtung (
Initial Event): Zeitstempel, Name/Kürzel des Finders, Notizen. - Optional 1–2 Belegfotos (
Media): Komprimierte Originalfotos (Content-Addressed per SHA-256).
flowchart LR
Finder[Erfasser vor Ort] -->|Fund teilen| Channel{Transportweg}
Channel -->|A: Offline vor Ort| QR[Einzel-QR-Code auf Display]
Channel -->|B: Messenger / Mail| File[Minipaket .discovery inkl. Foto]
Channel -->|C: Human-Readable Text| Text[WhatsApp Text mit Google Maps Link]
QR & File --> Receiver[Empfänger App]
Receiver --> Spatial{Spatial Check <10m?}
Spatial -- Ja --> Merge[Mit existierendem Objekt zusammenführen]
Spatial -- Nein --> Triage[Als 'pending_review' Neufund auf Karte markieren]
4.2. Die 3 Übertragungswege für Einzelfunde
Weg A: Der Offline Single-QR-Code (Sofort-Transfer von Display zu Kamera)
Wenn zwei Erfasser im Wald stehen und keine Fotos übertragen werden müssen:
- Die App rendert die Entitäts-Daten als einzelnen, statischen QR-Code.
- Payload-Format (komprimiertes JSON):
{ "type": "single_discovery", "entity": { "id": "018f4a21-7b3c-7000-85f2-9c1a7e123456", "lat": 48.45210, "lon": 8.31240, "props": { "title": "Neufund Waldweg", "status": "active" } }, "event": { "id": "018f4a25-11aa-7000-811b-123456789abc", "ts": "2026-10-11T14:25:00Z", "author": "Klaus", "note": "Frisch aufgeworfen" } } - Kollege scannt den QR-Code mit der App → Der Fund ist in unter 1 Sekunde in der lokalen Datenbank gespeichert.
Weg B: Messenger One-Click-Share (.discovery Datei inkl. Foto)
- Über den Button „Fund als Datei teilen“ erzeugt die App ein minimales Container-Paket (
fund_20261011.discoverybzw..sync). - Das Paket enthält das JSON der Entität sowie 1–2 Belegfotos (Dateigröße meist < 500 KB).
- Automatischer Intent-Filter: Tippt der Empfänger in WhatsApp oder Signal auf die Datei, öffnet sich sofort die Empfänger-App, zoomt auf den Fundort und zeigt das Belegfoto.
Weg C: Human-Readable Textnachricht (Für Empfänger ohne App)
Für Helfer oder Behörden, die die App nicht installiert haben, generiert die App einen lesbaren Nachrichtentext für das System-Share-Sheet:
📍 Neue Entdeckung gemeldet!
Objekt: Neufund Waldrand Distrikt 4
Koordinaten: 48.45210, 8.31240
Datum: 11.10.2026 14:25 Uhr
Finder: Klaus
Google Maps Link: https://www.google.com/maps?q=48.45210,8.31240
(Tippe auf den Dateianhang 'fund.discovery', um den Eintrag direkt in die App zu importieren.)
4.3. Qualitätskontrolle & Triage beim Empfänger
Wenn Einzelmeldungen von Dritten eingehen, müssen sie vom Koordinator oder Revierleiter verifiziert werden:
- Initialer Status:
- Jede importierte Einzelmeldung startet lokal mit
verification_status = 'pending_review'. - Auf der Karte wird der Pin farblich hervorgehoben (z. B. gelber Pin oder gestricheltes Symbol), damit er sofort als „noch ungeprüft“ auffällt.
- Jede importierte Einzelmeldung startet lokal mit
- Automatischer Spatial-Duplicate-Check:
- Befindet sich der gemeldete Neufund im Radius von 10–15 Metern um ein bereits bekanntes Objekt, meldet die App:
„Achtung: Neuer Fund liegt nur 7m neben existierender Station #42. Möchtest du ihn als neuen Kontrollbesuch zu Station #42 hinzufügen oder als eigenständiges Objekt anlegen?“
- Befindet sich der gemeldete Neufund im Radius von 10–15 Metern um ein bereits bekanntes Objekt, meldet die App:
- Bestätigung:
- Mit einem Klick auf „Bestätigen / Verifizieren“ wechselt der Status auf
verified.
- Mit einem Klick auf „Bestätigen / Verifizieren“ wechselt der Status auf
5. Paket-Struktur (.sync / Container-Spezifikation)
Sowohl für Gesamtexporte, Tour-Deltas als auch für Einzelfunde (.discovery) wird dieselbe standardisierte Archivstruktur verwendet:
export_20261011_143000.sync (ZIP-Archiv)
├── manifest.json # Metadaten, Generator-Info, Prüfsummen
├── data.json # Nutzdaten: Entities, Events, Aliases
└── media/ # Binärdateien, benannt nach Content-Hash
├── e3b0c442...jpg
└── 8f4a2199...m4a
5.1. manifest.json
{
"sync_spec_version": "1.0",
"package_id": "018f4a35-1234-7000-8000-abcdef012345",
"created_at": "2026-10-11T14:30:00Z",
"scope": "delta",
"entity_count": 3,
"event_count": 8,
"media_count": 2
}
5.2. data.json
{
"entities": [
{
"id": "018f4a21-7b3c-7000-85f2-9c1a7e123456",
"version": 3,
"updated_at": "2026-10-11T14:20:00Z",
"deleted_at": null,
"coordinates": { "latitude": 48.45210, "longitude": 8.31240 },
"properties": {
"title": "Station 4a",
"category": "type_alpha",
"status": "active"
}
}
],
"events": [
{
"id": "018f4a25-11aa-7000-811b-123456789abc",
"parent_entity_id": "018f4a21-7b3c-7000-85f2-9c1a7e123456",
"event_timestamp": "2026-10-11T14:25:00Z",
"author": "Klaus",
"observations": {
"measurement_value": 85.5,
"note": "Reguläre Kontrolle durchgeführt."
},
"media_hashes": [
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
]
}
],
"aliases": [
{
"alias_id": "018f4a30-9999-7000-aaaa-bbbbccccdddd",
"canonical_id": "018f4a21-7b3c-7000-85f2-9c1a7e123456",
"merged_at": "2026-10-11T14:28:00Z"
}
]
}
6. Räumliche Duplikate, Merging & Alias-Propagation
Wenn mehrere Erfasser unabhängig voneinander im Feld dasselbe physische Objekt erfassen, entstehen unterschiedliche UUIDs für denselben realen Ort.
flowchart TD
A[Eingehende Entität prüfen] --> B{UUID lokal bekannt?}
B -- Ja --> C[Standard Versions-Check Idempotent]
B -- Nein --> D{Liegt ID in Alias-Tabelle?}
D -- Ja --> E[Auf canonical_id umschreiben & Events anhängen]
D -- Nein --> F{Distanz zu existierender Entität kleiner als Toleranzradius z.B. 10m?}
F -- Nein --> G[Als neue Entität einfügen]
F -- Ja --> H[Spatial Duplicate Trigger]
H --> I[UI Dialog: Zusammenführen oder Separate Entität?]
I -- Zusammenführen --> J[Alias-Eintrag anlegen & Events/Medien verknüpfen]
6.1. Der Spatial Duplicate Check
- Beim Import berechnet die App die Haversine-Distanz zwischen den Koordinaten der neuen Entität und allen lokalen Entitäten.
- Liegt der Abstand unterhalb des Schwellenwerts (z. B.
10.0 Meter), wird ein Konflikt-Event ausgelöst.
6.2. Der Merge-Ablauf (Zusammenführung)
Wählt der Nutzer „Zusammenführen“:
- Kanonische ID festlegen: Eine ID wird als
canonical_idgewählt (z. B. die ältere oder vom Nutzer ausgewählte). - Untergeordnete Daten retten: Alle verknüpften
EventsundMediader unterlegenen ID werden auf diecanonical_idumgeschrieben:UPDATE events SET parent_entity_id = :canonical_id WHERE parent_entity_id = :alias_id; - Alias-Eintrag schreiben:
INSERT INTO entity_aliases (alias_id, canonical_id, merged_at) VALUES (:alias_id, :canonical_id, CURRENT_TIMESTAMP); - Alte Entität entfernen / entwerten: Die unterlegene Entität wird aus der aktiven Liste gelöscht.
6.3. Die Alias-Propagation (Vermeidung von Zombie-Duplikaten)
- Die
aliases-Tabelle reist in jedem weiteren Export mit. - Importiert ein Dritter später eine Datei, die noch auf die alte
alias_idverweist, schlägt seine App inaliasesnach, leitet die Daten sofort auf diecanonical_idum und verhindert die Wiederauferstehung des Duplikats.
7. Lokales Client-Schema (SQLite / Drift Referenz)
Jeder Peer implementiert lokal mindestens folgende Tabellenstruktur:
-- 1. Haupt-Entitäten (Stammdaten)
CREATE TABLE entities (
id TEXT PRIMARY KEY, -- UUIDv7
version INTEGER NOT NULL DEFAULT 1,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
verification_status TEXT NOT NULL DEFAULT 'verified', -- 'pending_review', 'verified'
properties_json TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
deleted_at TEXT -- Soft-Delete Zeitstempel
);
-- 2. Zeitreihen-Ereignisse (Append-Only)
CREATE TABLE events (
id TEXT PRIMARY KEY, -- UUIDv7
parent_entity_id TEXT NOT NULL, -- Verweis auf entities(id)
event_timestamp TEXT NOT NULL,
author TEXT,
observations_json TEXT NOT NULL,
FOREIGN KEY(parent_entity_id) REFERENCES entities(id)
);
-- 3. Medien-Metadaten (Content-Addressed)
CREATE TABLE media (
sha256 TEXT PRIMARY KEY, -- Hash des Datei-Inhalts
file_name TEXT NOT NULL,
mime_type TEXT NOT NULL,
byte_size INTEGER NOT NULL,
local_path TEXT NOT NULL -- Lokaler Pfad im App-Sandbox
);
-- 4. Verknüpfungstabelle Event <-> Media
CREATE TABLE event_media (
event_id TEXT NOT NULL,
media_sha256 TEXT NOT NULL,
PRIMARY KEY(event_id, media_sha256),
FOREIGN KEY(event_id) REFERENCES events(id),
FOREIGN KEY(media_sha256) REFERENCES media(sha256)
);
-- 5. Alias-Tabelle für gemergte Duplikate
CREATE TABLE entity_aliases (
alias_id TEXT PRIMARY KEY,
canonical_id TEXT NOT NULL,
merged_at TEXT NOT NULL
);
8. Zusammenfassung & Entwickler-Spickzettel
================================================================================
DEZENTRALE P2P & ASYNC SYNC SPEZIFIKATION – DEVELOPER CHEAT SHEET
================================================================================
1. IDENTITÄT & DATEN:
- Primärschlüssel: Immer clientseitige UUIDv7 (zeit-sortierbar).
- Medien-Dateien: Dateiname = SHA-256 Hash (Deduplizierung garantiert).
- Beobachtungen/Events: Immer Append-Only (erzeugen keine Schreibkonflikte).
- Löschungen: Niemals hartes DELETE, immer 'deleted_at: ISO8601'.
2. AD-HOC EINZELFUNDE (Single Discovery):
- Transfer: Einzel-QR-Code (vor Ort) oder Minipaket .discovery via Messenger.
- Triage: Startet beim Empfänger als 'pending_review' (auf Karte markiert).
- Spatial Check: Distanz < 10m -> Automatische Frage zum Zusammenführen.
3. ASYNCHRONER VERSAND (WhatsApp, E-Mail, Signal):
- Absender: Exportiert gefiltertes Delta (z.B. "Heutige Tour").
- Paket: ZIP-Archiv (.sync) mit manifest.json, data.json, media/.
- Messenger-Storage-Falle: WhatsApp speichert in /Android/media/ (nicht Downloads).
- Zero-Click Lösung: App registriert ACTION_VIEW/SEND Intent-Filter (Antippen = Direktimport).
- Empfänger: 1-Weg-Import mit idempotenter Prüfung:
* Bereits bekannt mit gleicher Version? -> Ignorieren.
* Neuer? -> Aktualisieren.
* Bild-Hash existiert lokal schon? -> Nicht neu entpacken.
4. ECHTZEIT P2P DIREKTABGLEICH (Feld / Hotspot / WLAN):
- Einseitiger Transfer (Sender -> Empfänger):
1. Sender wählt Scope / Datenpaket aus und startet lokalen Webserver mit Einmal-Token.
2. Empfänger scannt QR-Code mit Kamera und lädt Datenpaket herunter (GET /sync?token=...).
3. Empfänger prüft eigenständig & idempotent (neue/neuere Versionen übernehmen, ältere ignorieren).
4. Keine automatische Rückübertragung (rein einseitig, kein /upload-Endpunkt).
5. ZUSAMMENFÜHREN VON DUPLIKATEN (Spatial Merge):
- Koordinaten-Check: Distanz < 10m -> Duplicate-Warnung.
- Beim Merge: Unterlegene ID wandert in 'entity_aliases' (alias_id -> canonical_id).
- Alle Events und Fotos der unterlegenen ID werden an canonical_id angehängt.
- Aliase reisen in Sync-Paketen mit (verhindert Zombie-Duplikate im gesamten Team).
================================================================================
9. Referenz-Implementierung in MistelMonitor (App-Architektur)
Die vorliegende Spezifikation ist in der MistelMonitor-Applikation (de.mistelmonitor.app) vollständig integriert und operativ erprobt:
9.1. Datenbankschema & Persistenz (SQLite / v72 & v73)
entity_aliases(v72): Tabelleentity_aliases (alias_id TEXT PRIMARY KEY, canonical_id TEXT NOT NULL, merged_at TEXT NOT NULL).- Gekapselt in
AliasDao. - Verwaltet von
MergeListController: Bei räumlicher Duplikat-Zusammenführung wird die unterlegene ID erfasst und der ursprüngliche Eintrag perdeleted_atsoft-gelöscht (Non-Destructive). - Zombie-Schutz in
ImportService: Eingehende IDs werden überresolveCanonicalId()rekursiv auf die kanonische Ziel-ID umgebogen.
- Gekapselt in
- Content-Addressed Media Storage (v73): Tabelle
imagesum Spaltesha256 TEXTerweitert.ImageDao.getImageBySha256(hash)für O(1)-Prüfung existierender Fotos.ImageService.computeSha256()berechnet Hashes bei Aufnahme und Import.ImportServiceüberspringt das Entpacken bereits vorhandener Bilddateien auf Flash-Speicher.
9.2. Entkopplung über P2PSyncAdapter
Um das interne, stark typisierte Fachmodell (Report, Event, ReportImage) sauber von der Spezifikation zu trennen, erfolgt die Transformation exklusiv im P2PSyncAdapter:
reportToEntity(report)↔entityToReport(entity)aliasToGeneric(alias)↔genericToAlias(map)createGenericDataJson(...)generiert die spezifikationskonformedata.json.
9.3. Container-Formate & Dual-Writer
- Import (
ImportService):- Erkennt
.sync,.discoveryund.zipArchive. - Unterstützt sowohl
data.json(Generische Spezifikation) als auchadmin_data.json/reports.json(Legacy-Formate). - Extrahiert Belegfotos aus
media/undimages/.
- Erkennt
- Export (
ExportService):- Dual-Writer: Schreibt
data.json(Generic) UNDadmin_data.json(Legacy) in jedes generierte Paket. - Manifest enthält
"format": "generic-sync-packet-v1"und"sync_spec_version": "1.0".
- Dual-Writer: Schreibt
9.4. Einzelfund-Sharing (SingleDiscoveryService)
- Weg A (Offline Single-QR-Code): Rendert kompaktes JSON (
type: single_discovery) auf dem Display viaSingleDiscoveryQrDialog. - Weg B (Messenger Minipaket): Erzeugt
.discoveryZIP-Container mit Belegfotos. - Weg C (Menschlich lesbar): Formatierter Text mit Baumart, Befallsstufe und Google Maps Hyperlink.
- Scanner-Integration: Die Scanner-Komponente in
P2PSyncScreenerkennt Single-Discovery-QRs automatisch und importiert sie direkt vor Ort.
9.5. Einseitiger P2P-Sync (Sender → Empfänger, P2PSyncService)
- QR-Handshake: Sender startet lokalen Webserver und zeigt
http://$ip:$port/sync?token=$token. - Abruf & Übertragung (Sender → Empfänger): Empfänger scannt QR-Code und lädt das Paket per
GET /sync?token=$tokenherunter. - Prüfung & Import beim Empfänger: Empfänger führt Deduplizierungsprüfung & Import durch (
DataTransferService.importPackage). - Kein Rückkanal: Kein
/upload-Endpoint, keine automatische Rückübertragung von Client-Daten an den Sender. Der Datenaustausch ist strikt einseitig und kontrolliert.