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

  1. Zero-Infrastructure: Keine Serverkosten, keine Hosting-Wartung, keine Angriffsfläche für serverbasierte Datenlecks.
  2. Offline-First & Dezentral: Jedes Gerät ist autonomer Knotenpunkt (Peer) und verwaltet eine lokale Datenbank (z. B. SQLite / Drift).
  3. 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.
  4. 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) und updated_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.
  • 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 die canonical_id umgeschrieben.

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):
    1. Gold-Standard (Zero-Click Intent-Filter):
      • Die Client-App registriert im AndroidManifest.xml Intent-Filter für ACTION_VIEW und ACTION_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.
    2. System Share-Sheet („Teilen mit…“):
      • In WhatsApp lange auf die Datei tippen → Drei-Punkte-Menü → „Teilen“ → App auswählen.
    3. 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.

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:

  1. Volle Kontrolle beim Sender:
    • Der Sender entscheidet bewusst, wann und welche Daten bereitgestellt werden.
  2. 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.
  3. Keine unerwünschte Datenvermischung:
    • Da der Empfänger nicht automatisch ungefragt Daten zurücksendet, bleibt der Sender-Datenbestand unberührt.
  4. Keine WhatsApp-Bildkompression (Volle Bildauflösung):
    • P2P überträgt die originalen Belegfotos verlustfrei direkt von Gerät zu Gerät.
  5. 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.

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.“

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.discovery bzw. .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:

  1. 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.
  2. 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?“
  3. Bestätigung:
    • Mit einem Klick auf „Bestätigen / Verifizieren“ wechselt der Status auf verified.

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“:

  1. Kanonische ID festlegen: Eine ID wird als canonical_id gewählt (z. B. die ältere oder vom Nutzer ausgewählte).
  2. Untergeordnete Daten retten: Alle verknüpften Events und Media der unterlegenen ID werden auf die canonical_id umgeschrieben:
    UPDATE events SET parent_entity_id = :canonical_id WHERE parent_entity_id = :alias_id;
    
  3. Alias-Eintrag schreiben:
    INSERT INTO entity_aliases (alias_id, canonical_id, merged_at)
    VALUES (:alias_id, :canonical_id, CURRENT_TIMESTAMP);
    
  4. 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_id verweist, schlägt seine App in aliases nach, leitet die Daten sofort auf die canonical_id um 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): Tabelle entity_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 per deleted_at soft-gelöscht (Non-Destructive).
    • Zombie-Schutz in ImportService: Eingehende IDs werden über resolveCanonicalId() rekursiv auf die kanonische Ziel-ID umgebogen.
  • Content-Addressed Media Storage (v73): Tabelle images um Spalte sha256 TEXT erweitert.
    • 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 spezifikationskonforme data.json.

9.3. Container-Formate & Dual-Writer

  • Import (ImportService):
    • Erkennt .sync, .discovery und .zip Archive.
    • Unterstützt sowohl data.json (Generische Spezifikation) als auch admin_data.json / reports.json (Legacy-Formate).
    • Extrahiert Belegfotos aus media/ und images/.
  • Export (ExportService):
    • Dual-Writer: Schreibt data.json (Generic) UND admin_data.json (Legacy) in jedes generierte Paket.
    • Manifest enthält "format": "generic-sync-packet-v1" und "sync_spec_version": "1.0".

9.4. Einzelfund-Sharing (SingleDiscoveryService)

  • Weg A (Offline Single-QR-Code): Rendert kompaktes JSON (type: single_discovery) auf dem Display via SingleDiscoveryQrDialog.
  • Weg B (Messenger Minipaket): Erzeugt .discovery ZIP-Container mit Belegfotos.
  • Weg C (Menschlich lesbar): Formatierter Text mit Baumart, Befallsstufe und Google Maps Hyperlink.
  • Scanner-Integration: Die Scanner-Komponente in P2PSyncScreen erkennt 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=$token herunter.
  • 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.
Zurück zur Übersicht