Files
whiteboard-url-refs/docs/02-implementierungsplan.md
T

15 KiB
Raw Blame History

Whiteboard URL-Referenzen — Implementierungsplan

Status: Planungsphase · Ziel: Base64-Bilder durch URL-Referenzen ersetzen · Erwartete Wirkung: 50+ Bilder ohne OOM


Ziel

Statt jedes Bild als 50MB Base64-String im Excalidraw-JSON zu speichern, soll nur eine URL gespeichert werden. Der Browser lädt das Bild nur beim Hinscrollen in den Viewport.

// VORHER (Status Quo):
"files": {
  "abc123": {
    "dataURL": "data:image/png;base64,iVBORw0KGgoAAAAA...",  // ← 50 MB
    "mimeType": "image/png"
  }
}

// NACHHER (Ziel):
"files": {
  "abc123": {
    "url": "/remote.php/dav/files/Hermes/WhiteboardAssets/abc123.png",
    "thumbUrl": "data:image/png;base64,...",     // ← 5 KB Thumbnail (256px)
    "mimeType": "image/png",
    "width": 4000,
    "height": 3000
  }
}

RAM-Effekt:

  • Vorher: 50 Bilder × 50 MB = 2,5 GB 💥
  • Nachher: 50 Bilder × 5 KB (Thumbnail) + 3 geladene Vollbilder × 50 MB = ~150 MB

Architektur: 3 Eingriffspunkte

┌─────────────────────────────────────────────────────────────┐
│ BROWSER                                                     │
│                                                             │
│  1. IMPORT-HANDLER                                          │
│     Whiteboard JS (NcSelect)                                │
│     ┌──────────────────────────────────┐                    │
│     │ File dropped by user             │                    │
│     │   ├─ Upload full image to NC     │ ← NEU: WebDAV PUT │
│     │   ├─ Generate thumbnail (256px)  │ ← NEU: Canvas      │
│     │   └─ Store URL in file object    │ ← NEU              │
│     └──────────────────────────────────┘                    │
│                                                             │
│  2. JSON-SPEICHERUNG                                        │
│     Excalidraw data model                                   │
│     ┌──────────────────────────────────┐                    │
│     │ fileObject.url = "/dav/..."      │ ← NEU              │
│     │ fileObject.thumbUrl = "data:..." │ ← NEU (256px)      │
│     │ fileObject.dataURL = DEPRECATED  │                     │
│     └──────────────────────────────────┘                    │
│                                                             │
│  3. RENDERING                                               │
│     Excalidraw renderer                                     │
│     ┌──────────────────────────────────┐                    │
│     │ Viewport check → load from url?  │ ← NEU: Lazy Load   │
│     │ Not visible → show thumbnail     │ ← NEU: LOD         │
│     │ Zoom > 150% → load full image    │ ← NEU: Progressive │
│     └──────────────────────────────────┘                    │
└─────────────────────────────────────────────────────────────┘
                          │
                          │ WebDAV / Files API
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ NEXTCLOUD SERVER                                            │
│   ┌──────────────────────────────────┐                      │
│   │ /WhiteboardAssets/               │ ← NEU: Ordner        │
│   │   ├─ abc123.png                  │                      │
│   │   ├─ def456.jpg                  │                      │
│   │   └─ ...                         │                      │
│   │ CORS: Allow-Origin: *            │ ← NEU: Config        │
│   └──────────────────────────────────┘                      │
└─────────────────────────────────────────────────────────────┘

Vorbereitung: Test-Toolkit

Tool Zweck
curl Server-seitige Verifikation (CORS-Header, Datei-Upload, JSON-Inhalt)
Playwright Browser-Automation (Drag-Drop, Canvas-Screenshot, Console-Logs, RAM-Messung)
Chrome DevTools → Performance Monitor Live RAM/Task-Manager während manuellem Test
verify-whiteboard-patches.sh Regression: bestehende 6 JS-Patches noch aktiv?
Testbilder-Generator PNGs mit exakten Dimensionen (500×400 bis 8000×6000)

Testbilder generieren

ssh orange@niklashmotion.art "python3 -c \"
import struct, zlib
def create_png(w, h):
    def chunk(ctype, data):
        c = ctype + data
        crc = struct.pack('>I', zlib.crc32(c) & 0xffffffff)
        return struct.pack('>I', len(data)) + c + crc
    sig = b'\\\\x89PNG\\\\r\\\\n\\\\x1a\\\\n'
    ihdr = chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
    raw = b''
    for y in range(h):
        raw += b'\\\\x00' + bytes([(x+y) % 256 for x in range(w*3)])
    idat = chunk(b'IDAT', zlib.compress(raw))
    iend = chunk(b'IEND', b'')
    return sig + ihdr + idat + iend

sizes = [(500, 400, 'small'), (2000, 1500, 'medium'), (5000, 3750, 'large'), (8000, 6000, 'xl')]
for w, h, label in sizes:
    with open(f'/tmp/test-{label}-{w}x{h}.png', 'wb') as f:
        f.write(create_png(w, h))
    print(f'Created test-{label}-{w}x{h}.png')
\""

Phase 0: Baseline messen

Ziel: Aktuellen RAM-Verbrauch und Bildgrößen dokumentieren, um Verbesserung zu quantifizieren.

# 0.1 Bestehende Patches verifizieren
ssh orange@niklashmotion.art \
  "bash /opt/data/skills/devops/whiteboard-patches/scripts/verify-whiteboard-patches.sh"

# 0.2 RAM vor/nach Board-Öffnung (Chrome Task-Manager Shift+Esc)

# 0.3 Board-JSON-Größe: 1 Bild 2000×1500px → dataURL-Länge messen

Erwartet: 2000×1500px → ~12 MB Base64 · 5000×3750px → ~70 MB Base64 · 3 große Bilder → ~200 MB RAM → OOM-Risiko

🚦 GATE Phase 0

  • verify-whiteboard-patches.sh zeigt alle 6 Patches als aktiv
  • RAM-Messung dokumentiert (Baseline)
  • Testbilder existieren auf orange-desktop unter /tmp/test-*.png

Erst wenn alle 3 Checks grün sind → weiter zu Phase 1.


Phase 1: CORS + WebDAV-Zugriff

1.1 Whiteboard-Assets-Ordner anlegen

source /opt/data/home/.hermes/nextcloud.env
curl -s -u "$NEXTCLOUD_USER:$NEXTCLOUD_TOKEN" -X MKCOL \
  "$NEXTCLOUD_URL/remote.php/dav/files/$NEXTCLOUD_USER/WhiteboardAssets"

1.2 CORS für Bild-URLs aktivieren

Damit der Browser Bilder cross-origin laden kann:

# In /etc/nginx/sites-available/nextcloud.conf
location ~ \.(png|jpg|jpeg|gif|webp|svg)$ {
    add_header Access-Control-Allow-Origin "*";
}

Nginx reload: sudo nginx -s reload

1.3 Upload-Test

source /opt/data/home/.hermes/nextcloud.env
curl -s -u "$NEXTCLOUD_USER:$NEXTCLOUD_TOKEN" -X PUT \
  --data-binary @/tmp/test-small-500x400.png \
  "$NEXTCLOUD_URL/remote.php/dav/files/$NEXTCLOUD_USER/WhiteboardAssets/test.png"

🚦 GATE Phase 1

  • curl -sI "$URL" | grep "Access-Control-Allow-Origin"*
  • Upload per curl erfolgreich (HTTP 201)
  • Download per Browser möglich (kein CORS-Error in Console)
  • Playwright: new Image() mit crossOrigin lädt korrekt, naturalWidth === 500

Erst wenn alle 4 Checks grün sind → weiter zu Phase 2.


Phase 2: Import-Handler patchen

2.1 File-Drop-Handler in NcSelect finden

Im NcSelect-CknHatsl.chunk.mjs den Callback identifizieren, der aktuell reader.readAsDataURL() aufruft.

2.2 Durch WebDAV-Upload ersetzen

// STATT: reader.readAsDataURL(file) → dataURL
// NEU:
async function uploadToNextcloud(file, fileId) {
  const ext = file.name.split('.').pop();
  const url = `/remote.php/dav/files/${userId}/WhiteboardAssets/${fileId}.${ext}`;
  await fetch(url, { method: 'PUT', body: file, credentials: 'include' });
  return `/remote.php/dav/files/${userId}/WhiteboardAssets/${fileId}.${ext}`;
}

2.3 Thumbnail parallel generieren

// 256px Thumbnail, ~5 KB
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const scale = Math.min(256 / img.width, 256 / img.height);
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const thumbDataURL = canvas.toDataURL('image/jpeg', 0.7);

2.4 File-Objekt anpassen

// fileObject speichert URL statt dataURL
fileObject.url = uploadedUrl;
fileObject.thumbUrl = thumbDataURL;
fileObject.width = img.naturalWidth;
fileObject.height = img.naturalHeight;
// fileObject.dataURL = undefined (nicht mehr setzen!)

🚦 GATE Phase 2 (Playwright-Test)

// test-phase2.js — muss alle Checks bestehen:
const checks = {
  hasUrl: !!file?.url,              // ← Bild hat URL
  hasThumbUrl: !!file?.thumbUrl,    // ← Thumbnail generiert
  noDataUrl: !file?.dataURL,        // ← KEIN 50MB Base64 mehr
  ramDelta: ramAfter - ramBefore,   // ← < 5 MB (statt 50 MB)
  noErrors: errors.length === 0,    // ← Kein fileTooBig, kein CORS
};
// ALLE müssen true sein.
  • checks.hasUrl === true
  • checks.hasThumbUrl === true
  • checks.noDataUrl === true
  • checks.ramDelta < 5 * 1024 * 1024 (5 MB)
  • checks.noErrors === true
  • Board-JSON via curl: enthält "url", NICHT "dataURL"
  • verify-whiteboard-patches.sh zeigt alle 6 Patches weiterhin aktiv

Erst wenn alle 7 Checks grün sind → weiter zu Phase 3.


Phase 3: Excalidraw-Rendering patchen (LOD)

3.1 File-Objekt-Schema erweitern

Im percentages-BXMCSKIN-D6x-nnv3.chunk.mjs:

  • url als neues Feld im File-Objekt akzeptieren
  • dataURL als optional/Fallback behandeln

3.2 Lazy-Loading im Renderer

// PSEUDO-CODE
if (file.url && !imageLoaded) {
  // Thumbnail sofort rendern
  ctx.drawImage(thumbnailImg, x, y, w, h);

  // Zoom > 1.5 → Vollbild nachladen
  if (zoom > 1.5 && !fullImageLoading) {
    fullImageLoading = true;
    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.src = file.url;
    img.onload = () => {
      file._fullImageLoaded = true;
      fullImageLoading = false;
      // Re-render mit Vollbild
    };
  }
}

3.3 Viewport-Culling

function isInViewport(element, scrollX, scrollY, zoom, viewportW, viewportH) {
  const elX = element.x + scrollX;
  const elY = element.y + scrollY;
  return elX + element.width * zoom > -viewportW/2
      && elX < viewportW + viewportW/2
      && elY + element.height * zoom > -viewportH/2
      && elY < viewportH + viewportH/2;
}

🚦 GATE Phase 3 (Playwright-Test)

// test-phase3.js — Board mit 10 Bildern:
const state = {
  ramAllThumbnails: performance.memory.usedJSHeapSize,     // ← < 100 MB
  fullResCount: countWhere(files, f => f._fullImageLoaded), // ← 0 (nur Thumbnails)
};
// Reinzoomen auf Bild #1 (zoom = 2.0):
const afterZoom = {
  fullResCount: countWhere(files, f => f._fullImageLoaded), // ← 13 (sichtbare)
  thumbnailCount: countWhere(files, f => !f._fullImageLoaded), // ← 79
};
// Rauszoomen (zoom = 0.5):
const afterUnload = {
  ramAfter: performance.memory.usedJSHeapSize,              // ← nahe ramAllThumbnails
};
  • RAM 10 Thumbnails < 100 MB
  • Vor Zoom: 0 Vollbilder geladen
  • Nach Zoom 2.0×: 13 Vollbilder geladen, Rest Thumbnails
  • Nach Zoom 0.5×: RAM sinkt (Vollbilder entladen)
  • Kein visuelles Flackern bei Thumbnail→Vollbild-Wechsel
  • verify-whiteboard-patches.sh weiterhin grün

Erst wenn alle 6 Checks grün sind → weiter zu Phase 4.


Phase 4: Migration + Stress-Test

4.1 Bestehende Boards migrieren

// Beim Öffnen: dataURL → Blob → WebDAV-Upload → url setzen
for (const [fileId, file] of Object.entries(files)) {
  if (file.dataURL && !file.url) {
    const blob = dataURLtoBlob(file.dataURL);
    const url = await uploadToNextcloud(blob, fileId);
    file.url = url;
    file.thumbUrl = await generateThumbnail(blob);
    delete file.dataURL;
  }
}

4.2 50-Bilder-Stress-Test

// Playwright: 50 Bilder nacheinander importieren
for (let i = 0; i < 50; i++) {
  await page.dropFile('canvas', '/tmp/test-medium-2000x1500.png');
  await page.waitForTimeout(500);
}
const ram = await page.evaluate(() => performance.memory.usedJSHeapSize);
// Speichern + neu laden + Prüfen: kein Datenverlust

🚦 GATE Phase 4

  • Bestehendes Board mit dataURL-Bildern öffnet OHNE Fehler
  • Nach Migration: Board-JSON via curl → alle Bilder haben url, keine dataURL
  • 50 Bilder importiert → RAM < 200 MB
  • Board speichern + neu laden → alle 50 Bilder sichtbar (Thumbnails)
  • Netzwerk-Fehler-Simulation: Bild lädt auch ohne WebDAV (Thumbnail-Fallback)
  • verify-whiteboard-patches.sh weiterhin grün

Entscheidungen (vor Implementierung zu klären)

Frage Optionen
Wo Bilder speichern? A) /WhiteboardAssets/ (eigener Ordner) · B) gleicher Ordner wie Whiteboard-Datei
Thumbnail-Größe? 256px (5 KB) vs. 512px (12 KB)
Lazy-Load-Zoom-Schwelle? 1.0× (immer lazy) vs. 1.5× vs. 2.0×
Auth-Methode im JS? A) Session-Cookie · B) App-Passwort hardcoded
Migration automatisieren? Ja (beim Öffnen) vs. Nein (nur neue Bilder)

Aufwandsschätzung

Phase Aufwand Tests
Phase 1: CORS + WebDAV 30 min 4 Checks
Phase 2: Import-Handler 24 h 7 Checks
Phase 3: Rendering/LOD 48 h 6 Checks
Phase 4: Migration + Stress 12 h 6 Checks
Total 12 Tage 23 Gates

Failure Modes (worauf achten)

Symptom Ursache Prüfung
hasUrl = false nach Import Import-Patch nicht aktiv Console-Log auf Fehler
CORS-Error in Console Nginx-CORS-Header fehlt curl -sI | grep Access-Control
Bild nicht sichtbar crossOrigin nicht gesetzt img.crossOrigin = 'anonymous'
RAM steigt trotz URLs Vollbild sofort geladen Viewport-Check debuggen
Board crasht beim Öffnen Migration schlägt fehl try/catch + Fallback auf dataURL
Browser-Cache zeigt alte JS Cachebuster nicht erhöht curl -sI | grep cachebuster
Bestehende Patches kaputt Seiteneffekte in Bundle verify-whiteboard-patches.sh