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

407 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
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.
```bash
# 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
```bash
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:
```nginx
# 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
```bash
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
```javascript
// 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
```javascript
// 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
```javascript
// 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)
> ```javascript
> // 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
```javascript
// 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
```javascript
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)
> ```javascript
> // 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
```javascript
// 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
```javascript
// 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` |