Zum Inhalt

Datenmodell

Stand: nach T04 und T23. Die App speichert ihre Nutzdaten auf Android in der SQLite-Datenbank offgrid. Im Browser tritt an ihre Stelle eine gleichwertige Implementierung im Arbeitsspeicher. Beide Varianten verwenden dieselben abstrakten Repositories.

Beziehungen

erDiagram
    CONTACTS ||--o{ MESSAGES : "contact_id"
    CONTACTS ||--o{ COORDINATES : "contact_id"
    MESSAGES o|--o{ COORDINATES : "message_id, optional"

    CONTACTS {
        int contact_id PK
        string name
        string node_id
        string public_key UK
        string state
        string fingerprint
        int last_rssi
        float last_snr
        int tx_ctr
        int rx_highest_ctr
    }

    MESSAGES {
        string id PK
        int contact_id FK
        int ctr
        int msg_ref
        string content
        string direction
        string kind
        string sent_at
        int status
        int rssi
        string read_at
        string created_at
        string updated_at
    }

    COORDINATES {
        string id PK
        int contact_id FK
        string message_id FK
        float latitude
        float longitude
        float accuracy
        string context
        string share_id
        string share_until
        string recorded_at
        string created_at
        string updated_at
    }

    SETTINGS {
        string key PK
        string value
    }

Die Kontakte gehören der Antenne: sie besitzt die Schlüssel, die Zähler und die Kontaktliste. Die App spiegelt jeden Kontakt (ohne Schlüssel), damit Verlauf und Anzeige auch ohne Antenne da sind. Es gibt keine Tabelle für das Pairing: das macht die Antenne.

Schema und Migrationen

src/app/core/data/migrations.ts enthält die Migrationen in aufsteigender Versionsfolge. T04 legte Version 1 an; T23 hat sie einmalig umgeschrieben (Schema ohne Kontaktschlüssel), weil es noch keine veröffentlichte App und keine Nutzerdaten gab. Wer ein altes Schema auf dem Handy hat, deinstalliert die App. SqliteDatabaseService.open() öffnet die Datenbank offgrid ohne Verschlüsselung, aktiviert mit PRAGMA foreign_keys = ON die Fremdschlüssel und liest die gespeicherte Schema-Version aus PRAGMA user_version. Jede neuere Migration läuft in einer Transaktion. Danach wird PRAGMA user_version auf die neue Version gesetzt.

Die Spalten heissen in SQLite snake_case, im TypeScript-Modell camelCase. Das Mapping liegt in src/app/core/data/sqlite-mapping.ts. Laufzeitwerte werden mit ? als Parameter gebunden. Nur die bereits geprüfte Ganzzahl für PRAGMA user_version steht direkt im Statement.

Tabellen

contacts

Feld Typ Regeln und Bedeutung
contact_id INTEGER Primärschlüssel: der Platz 1 bis 32 auf der Antenne, stabil, solange der Kontakt dort existiert.
name TEXT Nicht null, 1 bis 24 Zeichen.
node_id TEXT Nicht null, genau 4 Zeichen (Hex, Grossbuchstaben).
public_key TEXT Nicht null, genau 44 Zeichen (Base64 von 32 Bytes); eindeutiger Index. Ein öffentlicher Schlüssel.
state TEXT Nur pending oder active. pending, bis das erste gültige Paket des Gegenübers eintrifft.
fingerprint TEXT Nicht null; drei Vierergruppen, z. B. „0501 3010 7790“. Auf beiden Handys gleich.
last_rssi INTEGER Optionale Signalstärke des letzten gültigen Pakets.
last_snr REAL Optionales Signal-Rausch-Verhältnis.
tx_ctr INTEGER Nächster ausgehender Zähler, 0 bis 16777215 (nur zur Information).
rx_highest_ctr INTEGER Höchster geprüfter Zähler, den die Antenne empfangen hat; leer, wenn noch keiner.

Es gibt keine Spalten für Sende- oder Empfangsschlüssel. Die Schlüssel eines Kontakts verlassen die Antenne nie.

messages

Feld Typ Regeln und Bedeutung
id TEXT Primärschlüssel, nicht null.
contact_id INTEGER Fremdschlüssel auf contacts.contact_id; Löschen des Kontakts löscht die Nachricht.
ctr INTEGER Zähler der Nachricht in der Unterhaltung (0 bis 16777215); bei ausgehenden Nachrichten erst nach der Antwort auf msg.send bekannt.
msg_ref INTEGER Lokaler Handle der Antenne für msg.status; nur bis zu ihrem nächsten Neustart gültig.
content TEXT Nicht null; bei einer Standort-Nachricht leer.
direction TEXT Nur in oder out.
kind TEXT Nur text oder location, Standard text.
sent_at TEXT Zeitpunkt, den die App setzt (die Antenne hat keine Uhr), nicht null.
status INTEGER 0 bis 3: queued, sent, delivered oder failed.
rssi INTEGER Optionale Signalstärke.
read_at TEXT Optionaler Zeitpunkt, an dem die Nachricht gelesen wurde.
created_at TEXT Erstellungszeitpunkt, nicht null.
updated_at TEXT Optionaler Änderungszeitpunkt.

Indizes: messages(contact_id, sent_at), messages(contact_id, direction, ctr) und messages(msg_ref).

Der Status der Antenne (queued, sent, relayed, retrying, delivered, failed, cancelled) wird auf die vier Werte abgebildet: queued → 0, sent/relayed/retrying → 1, delivered → 2, failed/cancelled → 3.

coordinates

Feld Typ Regeln und Bedeutung
id TEXT Primärschlüssel, nicht null.
contact_id INTEGER Fremdschlüssel auf contacts.contact_id; Löschen des Kontakts löscht die Koordinate.
message_id TEXT Optionaler Fremdschlüssel auf messages.id; Löschen der Nachricht löscht die Koordinate.
latitude REAL Nicht null, zwischen -90 und 90.
longitude REAL Nicht null, zwischen -180 und 180.
accuracy REAL Optionale Genauigkeit.
context TEXT Nur einmal oder live, Standard einmal.
share_id TEXT Optionale Id einer Standortfreigabe.
share_until TEXT Optionaler Zeitpunkt, bis zu dem die Freigabe läuft.
recorded_at TEXT Aufnahmezeitpunkt, nicht null.
created_at TEXT Erstellungszeitpunkt, nicht null.
updated_at TEXT Optionaler Änderungszeitpunkt.

Index: coordinates(message_id, recorded_at).

settings

Feld Typ Regeln und Bedeutung
key TEXT Primärschlüssel, nicht null.
value TEXT Nicht null.

Gespeicherte Schlüssel sind install_id, display_name, onboarding_done, bg_receive und vibrate.

Löschregeln und Gesamtoperationen

SQLite verwendet ON DELETE CASCADE: Beim Löschen eines Kontakts verschwinden seine Nachrichten und Koordinaten. Beim Löschen einer Nachricht verschwinden ihre Koordinaten. Die Memory-Repositories bilden dieselbe Kaskade ab.

DatabaseService.replaceAll(data) ersetzt die Nutzdaten in einer Transaktion. Es werden zuerst Koordinaten, Nachrichten und Kontakte gelöscht und danach die übergebenen Datensätze wieder eingefügt. Anschliessend wird display_name in den Einstellungen gesetzt; die übrigen Einstellungen bleiben erhalten.

DatabaseService.deleteAllUserData() löscht Koordinaten, Nachrichten und Kontakte. Die Einstellungen bleiben erhalten.

Browser-Daten und Demo-Daten

Die Memory-Implementierungen speichern ihre Datensätze in Maps. Sie starten leer und verwenden dieselbe Sortierung und dieselben Filter wie die SQLite-Implementierungen.

MemoryDatabaseService.seedDemo() füllt die Browser-Datenbank mit vier Kontakten (Anna Meier, Luca Bernasconi, Mia Frei und der noch ausstehende Jonas Keller) und sechs Nachrichten. Der Seed enthält keine Standortdaten. Die Demo-Einstellungen sind install_id = 'demo-install-id', display_name = 'HolyG', onboarding_done = '1', bg_receive = '1' und vibrate = '0'. Die Kontakte stehen in DEMO_CONTACTS (memory/seed.ts) mit festen, erfundenen öffentlichen Schlüsseln und Fingerabdrücken; das simulierte Gerät kennt dieselben Kontakte, sonst ersetzt die Synchronisation sie beim Verbinden.

Der App-Start ruft seedDemo() nur im Browser und nur auf, wenn localStorage.getItem('og.noseed') !== '1'.