Files
kapteins-daagbok/docs/backup-format-v2.md
elpatron 6f28ea0b16 Replace logbook backup v1 JSON with v2 ZIP archives.
ZIP .daagbok files use a compact manifest and binary KDAB blobs so large
photo, voice, and GPS payloads no longer inflate in a single JSON file.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 15:13:51 +02:00

14 KiB
Raw Permalink Blame History

Backup-Format v2 — Design

Status: Implementiert in feature/backup-format-v2 (BACKUP_VERSION = 2, Datei *.daagbok).

Ziel: Logbuch-Backups skalieren für viele Reisetage, Fotos, Voice-Memos und GPS-Tracks — ohne den gesamten Inhalt als eine große JSON-Datei im Browser-RAM zu halten.

Ausgangslage: v1 (BACKUP_VERSION = 1, Datei *.daagbok.json) serialisiert alle Payloads in ein einziges JSON-Objekt mit Pretty-Print. Binärdaten stecken doppelt als Base64-Strings in encryptedData. Import nutzt file.text() + JSON.parse() auf der vollen Datei.

Entscheidung: Keine Abwärtskompatibilität zu v1 — es gibt noch keine produktiven User-Backups. v1-Code und -json-Dateiendung wurden durch v2 ersetzt.


1. Anforderungen

Funktional

ID Anforderung
B-01 Export enthält alle lokalen Logbuch-Payloads: yacht, deviation, crews, entries, photos, voiceMemos, gpsTracks, logbookCrewSelection, logbookVesselSelection, nmeaArchives.
B-02 E2E-Verschlüsselung bleibt: Logbuch-Key mit Backup-Passphrase (PBKDF2) gewrappt; Payload-Blobs unverändert wie in Dexie (AES-GCM über encryptJson).
B-03 Medien (Fotos, Voice-Memos) als binäre Blob-Dateien im Archiv, nicht als Base64 in JSON.
B-04 Strukturdaten (Manifest, kleine Metadaten) als kompaktes JSON (ein Zeile, kein Pretty-Print).
B-05 Gesamtarchiv DEFLATE-komprimiert (ZIP).
B-06 Preview (Titel, Counts, Export-Datum) ohne vollständiges Entpacken aller Medien — nur Manifest + Key-Entschlüsselung.
B-07 Restore-Optionen wie heute: overwrite, assignNewId, Konflikt bei gleicher ID.
B-08 Nach Restore: Sync-Queue wie heute befüllen, optional syncLogbook wenn online.
B-09 Export vor Download optional mit Fortschrittsanzeige (Anzahl Blobs / Bytes).

Nicht-Ziele (v2)

  • Inkrementelles / dedupliziertes Backup über mehrere Dateien.
  • Backup auf dem Server (nur lokaler Download wie heute).
  • Klartext-Manifest oder unverschlüsselte Medien.
  • Account-weites Multi-Logbuch-Archiv (weiterhin ein Logbuch pro Datei).

Akzeptanzkriterien (UAT)

  1. Logbuch mit 50 Fotos + 20 Voice-Memos exportieren → Datei .daagbok deutlich kleiner als vergleichbares v1-JSON (Kompression + kein Pretty-Print + binäre Blobs).
  2. Restore auf frischem Gerät (eingeloggt) → alle Einträge, Medien abspielbar, Crew/Vessel-Selection vorhanden.
  3. Falsches Passphrase → BACKUP_WRONG_PASSPHRASE (wie heute).
  4. Beschädigtes ZIP / fehlende Manifest → BACKUP_INVALID_FORMAT.
  5. v1-Datei (version: 1) → BACKUP_VERSION_UNSUPPORTED mit Hinweis.

2. Container: ZIP-Archiv

Eigenschaft Wert
MIME-Typ (Download) application/vnd.kapteins-daagbok+zip (Fallback: application/zip)
Dateiendung .daagbok (kein .json)
Kompression ZIP mit DEFLATE (Level 6 — Balance Größe/Geschwindigkeit)
Magic / Erkennung ZIP-Signatur PK\x03\x04 + manifest.json mit format + version: 2

Bibliothek (Client): fflate (klein, ESM, ZIP sync/async). Als direkte dependencies-Eintrag in client/package.json, nicht nur transitiv.

Warum ZIP und nicht nur gzip auf einer JSON-Datei?

  • Viele unabhängige Blobs → paralleles Entpacken, Preview ohne alle Medien zu lesen.
  • Manifest bleibt klein (< 100 KB typisch).
  • Standard-Tooling (optional manuelle Inspektion mit unzip -l).

3. Archiv-Layout

backup.daagbok (ZIP)
├── manifest.json          # Klartext-Metadaten + Index (kompakt)
├── key.enc                # Mit Backup-Passphrase gewrapptes Logbuch-Key (binär)
├── logbook.meta.json      # encryptedTitle, id, updatedAt, isDemo (klein, JSON)
└── payloads/
    ├── yacht.enc
    ├── deviation.enc
    ├── logbook-crew.enc
    ├── logbook-vessel.enc
    ├── crews/
    │   └── {payloadId}.enc
    ├── entries/
    │   └── {payloadId}.enc
    ├── photos/
    │   └── {payloadId}.enc      # + sidecar optional: {payloadId}.meta.json
    ├── voice-memos/
    │   └── {payloadId}.enc
    ├── gps-tracks/
    │   └── {entryId}.enc
    └── nmea-archives/
        └── {entryId}.enc

3.1 manifest.json (Schema)

{
  "format": "kapteins-daagbok-backup",
  "version": 2,
  "exportedAt": "2026-06-03T12:00:00.000Z",
  "appVersion": "0.1.0.109",
  "compression": "zip-deflate-6",
  "logbookId": "uuid",
  "counts": {
    "entries": 42,
    "photos": 50,
    "voiceMemos": 12,
    "crews": 3,
    "gpsTracks": 40,
    "nmeaArchives": 2,
    "hasYacht": true,
    "hasDeviation": true,
    "hasLogbookCrewSelection": true,
    "hasLogbookVesselSelection": true
  },
  "totalUncompressedBytes": 125000000,
  "files": {
    "key": "key.enc",
    "logbook": "logbook.meta.json",
    "yacht": "payloads/yacht.enc",
    "deviation": null,
    "logbookCrewSelection": "payloads/logbook-crew.enc",
    "logbookVesselSelection": "payloads/logbook-vessel.enc",
    "crews": [
      { "payloadId": "…", "path": "payloads/crews/….enc", "updatedAt": "…" }
    ],
    "entries": [  ],
    "photos": [
      { "payloadId": "…", "entryId": "…", "path": "…", "updatedAt": "…", "bytes": 183422 }
    ],
    "voiceMemos": [  ],
    "gpsTracks": [  ],
    "nmeaArchives": [  ]
  }
}
  • appVersion: optional, aus Client-Build (PWA-Version) — nur für Support/Debug.
  • totalUncompressedBytes: Summe der Blob-Größen vor ZIP — für UI („~120 MB“) und Speicher-Warnung vor Import.
  • Keine encryptedData / IV / Tag im Manifest für große Blobs — nur im Binärformat (siehe 3.2).

3.2 Binärformat .enc (einheitlich für alle Payloads)

Jede .enc-Datei ist roh, kein JSON:

Offset  Size     Inhalt
0       4        Magic ASCII "KDAB" (Kaptein's Daagbok)
4       1        Format version = 1
5       12       IV (AES-GCM, wie heute)
17      16       Auth tag (AES-GCM)
33      N        Ciphertext (identisch mit heutigem decryptJson-Eingang:
                  Base64-decode von encryptedData + concat mit tag in decryptBuffer)

Migration von Dexie: Beim Export aus encryptedData (Base64-String) + iv + tag (Base64) → einmal decodieren → .enc-Datei schreiben. Beim Import umgekehrt → Dexie-Felder wie heute.

Vorteil: ~33 % weniger Speicher als Base64-in-JSON; Parser liest nur Header + Länge.

key.enc: Gleiches Binärformat, Inhalt = encryptBuffer(logbookKey, passphraseDerivedKey) — ersetzt das JSON-Objekt logbookKey: { ciphertext, iv, tag } aus v1.

logbook.meta.json: Unverändert kleines JSON (nur encryptedTitle, updatedAt, isDemo) — kein Binärbedarf.

3.3 Optionale Sidecars (Phase 2, nicht blocking v2)

Für Photos/Voice-Memos könnte {id}.meta.json nur { entryId, updatedAt } enthalten, falls das Manifest zu groß wird (>10k Medien). v2 startet mit allen Metadaten im Manifest — ausreichend bis ~einige tausend Dateien.


4. Kryptographie (unverändert in der Semantik)

Element v1 v2
Logbuch-Key im Backup PBKDF2 + AES-GCM (KapteinsDaagbokBackupFileSalt_v1) Gleich (Salt-String beibehalten für gleiche Passphrase → gleicher Key)
Payload-Verschlüsselung encryptJson mit Logbuch-Key Byte-für-byte gleicher Ciphertext in .enc
Passphrase-Mindestlänge 8 Zeichen 8 Zeichen

Optional in v2.1 (nicht v2): PBKDF2-Iterationen erhöhen (z. B. 310_000) mit neuem Salt …_v2 und Feld keyWrap: "pbkdf2-v2" in Manifest — nur wenn gewünscht.


5. Ablauf Export

sequenceDiagram
  participant UI as LogbookBackupPanel
  participant Svc as logbookBackupV2
  participant DB as Dexie
  participant ZIP as fflate ZIP

  UI->>Svc: exportLogbookBackup(logbookId, passphrase)
  Svc->>DB: collect all tables (batched)
  loop each payload
    Svc->>Svc: base64 → KDAB .enc bytes
    Svc->>ZIP: add file (deflate)
  end
  Svc->>ZIP: manifest.json + key.enc + logbook.meta.json
  Svc->>UI: Blob + filename.daagbok

Implementierungsdetails

  1. Pre-sync: wie heute syncLogbook(logbookId) wenn online.
  2. Sammlung: collectLogbookPayloadsV2() — alle Tabellen aus Abschnitt B-01; Batches à 20 für Medien.
  3. ZIP-Erzeugung: fflate zipSync oder zip mit Streaming-Callback; nicht das gesamte Archiv als ein Array im RAM, wenn totalUncompressedBytes > 80_000_000 → Warnung in UI + ggf. requestIdleCallback zwischen Blobs.
  4. Fortschritt: onProgress({ phase, current, total, bytes }).
  5. Download: downloadBackupBlob(blob, ${safeTitle}-${date}.daagbok).

Speicher-Richtwerte (UI-Warnung)

Uncompressed Empfehlung
< 50 MB Normal exportieren
50150 MB Hinweis: Import kann auf schwachen Geräten dauern
> 150 MB Bestätigungsdialog; Export trotzdem erlauben

ZIP reduziert typisch Medien-Anteil um 2040 % (JPEG/WebM komprimieren schlecht in ZIP, aber Base64-Overhead entfällt).


6. Ablauf Import / Preview

Preview (Passphrase-Check)

  1. ZIP öffnen (nur zentrales Directory lesen — fflate unzip).
  2. manifest.json parsen → version === 2 prüfen.
  3. key.enc laden → Passphrase → Logbuch-Key.
  4. logbook.meta.json → Titel entschlüsseln.
  5. Counts aus Manifest anzeigen — keine Medien-Blobs dekodieren.

Restore

  1. Vollständig entpacken in temporäre Struktur (Object-URLs / Map<path, Uint8Array>) — bei >150 MB Warnung.
  2. writeBackupToDexieV2() — analog v1, aber aus .enc Bytes.
  3. queueRestoredLogbookForSync() — unveränderte Sync-Queue-Semantik.
  4. listCache auf Entries: nach Restore optional aus Entry-Payload neu ableiten (wie bei normalem Decrypt) oder beim Export mit speichern — Design: listCache beim Export nicht sichern (bleibt abgeleitetes Feld); nach Restore beim ersten Öffnen neu berechnen.

Fehlercodes (neu/angepasst)

Code Bedeutung
BACKUP_VERSION_UNSUPPORTED version !== 2 (inkl. v1)
BACKUP_INVALID_ARCHIVE Kein ZIP / kein manifest
BACKUP_MISSING_BLOB Index verweist auf fehlende Datei
BACKUP_INVALID_ENC Magic/ Länge ungültig
(bestehend) BACKUP_WRONG_PASSPHRASE, BACKUP_ID_CONFLICT, …

7. Code-Struktur (Implementierung)

Datei Aufgabe
client/src/services/logbookBackup/encBlob.ts dexieRecordToEncBytes, encBytesToDexieFields
client/src/services/logbookBackup/manifest.ts Typen BackupManifestV2, Validierung
client/src/services/logbookBackup/zipArchive.ts ZIP pack/unpack mit fflate
client/src/services/logbookBackup.ts Öffentliche API: exportLogbookBackup, parseLogbookBackupFile, preview…, restore… — ruft v2 intern auf
client/src/components/LogbookBackupPanel.tsx .daagbok-Accept, Fortschrittsbalken, Größen-Warnung
i18n settings.backup_* Texte für v2, BACKUP_VERSION_UNSUPPORTED
docs/plausible-events.md Properties bytes, counts bei Export/Restore

v1 entfernen: BACKUP_VERSION = 1, LogbookBackupFile-Monolith, JSON.stringify(…, null, 2), normalizeBackupPayloads für voiceMemos — löschen, nicht parallel halten.


8. Vergleich v1 → v2

Aspekt v1 v2
Container Eine JSON-Datei ZIP .daagbok
Medien im Export Base64 in JSON-Strings Binäre .enc-Dateien
Manifest Alles in einem Objekt Schlankes manifest.json + Blobs
Kompression Keine (+ Pretty-Print) DEFLATE
RAM Import file.text() + full parse ZIP directory + gezieltes Entpacken; Preview nur Manifest
Fehlende Payloads Kein crew/vessel selection, kein NMEA Vollständig
Abwärtskompatibel Nein (bewusst)

Größenbeispiel (Schätzung)

100 Fotos à ~250 KB verschlüsselt (≈190 KB Ciphertext):

  • v1 JSON: ~100 × (190 KB × 4/3 Base64) ≈ 25 MB nur Fotos-Ciphertext-Strings + JSON-Escaping + Pretty-Print → oft 35+ MB
  • v2 ZIP: ~100 × 190 KB + Kompression ≈ 1922 MB Archiv

9. Implementierungsplan (Phasen)

Phase 1 — Kern (MVP v2)

  • encBlob + manifest + zipArchive Module
  • Export/Import/Preview/Restore auf v2
  • Alle Dexie-Tabellen inkl. crew/vessel selection + nmeaArchives
  • UI: .daagbok, Fehler BACKUP_VERSION_UNSUPPORTED für v1-JSON
  • Tests: encBlob unit tests

Phase 2 — UX & Robustheit

  • Export-Fortschritt
  • Größen-Warnung vor Import (>150 MB)
  • onProgress während Restore

Phase 3 — Optional

  • Streaming-Export für sehr große Archive (fflate async + Chunk-Schreiben)
  • PBKDF2 v2 mit höheren Iterationen
  • Sidecar-Metadaten wenn Manifest > 2 MB

10. Testplan

Test Typ
encBlob: round-trip Dexie-Felder ↔ Bytes Unit
Manifest-Validator: version 2, fehlende paths Unit
Export → unzip → Manifest-Counts = DB-Counts Integration (Vitest + IndexedDB fake)
Restore → decrypt photo/voice → gültige Data-URL Integration
v1-JSON-Datei → BACKUP_VERSION_UNSUPPORTED Unit
Korruptes ZIP → BACKUP_INVALID_ARCHIVE Unit

11. Dokumentation & Analytics

  • Nutzer-Texte: „Sicherungsdatei .daagbok“ statt .daagbok.json.
  • Plausible Backup Exported / Restored: Properties bytes, photos, voiceMemos (Anzahlen, keine Inhalte).
  • Deployment: kein Server-Change — Backup ist rein clientseitig.

12. Offene Punkte (für Review)

  1. Schwellwert Speicher-Warnung: 150 MB uncompressed — anpassen nach ersten Real-Logbüchern?
  2. NMEA-Archive: oft groß — eigenes Subdirectory ausreichend; später ggf. „NMEA nicht ins Backup“ als Opt-out?
  3. Geteilte Logbücher (isShared === 1): Export weiterhin nur Owner — unverändert.

Implementierung: client/src/services/logbookBackup/, API in client/src/services/logbookBackup.ts.