commit 5a7db008ee8f30dcf68d3a07d89b619f13984340 Author: Hermes Agent Date: Thu Jul 2 15:29:19 2026 +0000 Initial: Projektstruktur, Referenz-Dokument, Implementierungsplan, Test-Skripte diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f83c6a --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Whiteboard URL-Referenzen + +> **Ziel:** Nextcloud Whiteboard von Base64-Bildern auf URL-Referenzen umstellen — 50+ Bilder ohne Browser-OOM. + +## Problem + +Das Nextcloud Whiteboard (Excalidraw) speichert jedes importierte Bild als Base64-Data-URL inline im Board-JSON. Ein 4000×3000px Bild belegt ~50 MB RAM. Ab 3 Bildern crasht Chrome mit Out-of-Memory. + +## Lösung + +Statt Base64 wird das Bild als Datei in Nextcloud gespeichert. Im Board-JSON steht nur eine URL. Der Browser lädt Vollbilder nur bei Bedarf (lazy, beim Reinzoomen) — ähnlich wie Miro. + +## Projektstruktur + +``` +whiteboard-url-refs/ +├── README.md ← diese Datei +├── docs/ +│ ├── 01-reference.md ← Recap aller bestehenden Patches +│ └── 02-implementierungsplan.md ← 4-Phasen-Plan + 23 Test-Gates +├── patches/ +│ └── (kommende JS-Patches für Phase 2–4) +└── test-scripts/ + ├── generate-test-images.sh ← Testbilder (500–8000px) + ├── verify-phase1.sh ← CORS + WebDAV prüfen + └── (weitere Test-Skripte pro Phase) +``` + +## Phasen + +| Phase | Beschreibung | Status | +|---|---|---| +| **0** | Baseline: RAM messen, bestehende Patches verifizieren | ⬜ | +| **1** | CORS + WebDAV: Ordner anlegen, Cross-Origin erlauben | ⬜ | +| **2** | Import-Handler: Bild als Datei speichern, Thumbnail generieren | ⬜ | +| **3** | Rendering/LOD: Lazy-Loading, Viewport-Culling | ⬜ | +| **4** | Migration: Bestehende Boards konvertieren, Stress-Test | ⬜ | + +Jede Phase hat ein **🚦 GATE** — alle Checks müssen grün sein, bevor die nächste beginnt. + +## Setup + +```bash +git clone http://192.168.178.94:3000/Hermes/whiteboard-url-refs.git +``` diff --git a/docs/01-reference.md b/docs/01-reference.md new file mode 100644 index 0000000..ca0dc80 --- /dev/null +++ b/docs/01-reference.md @@ -0,0 +1,198 @@ +# Whiteboard Image Limits — Komplettdokumentation + +> **Letztes Update:** 2026-07-02 · **Status:** 6 JS-Patches aktiv, OOM-Problem identifiziert, URL-Referenzen als Lösung geplant + +--- + +## 1. Problem + +Das Nextcloud Whiteboard (Excalidraw) hat mehrere hartkodierte Limits für Bildimporte: + +| Limit | Default | Effekt | +|---|---|---| +| Downscale auf max. Kantenlänge | **1440px** (Excalidraw) + **1440px** (Whiteboard-eigener `image-blob-reduce`) | Bilder schrumpfen auf Briefmarken-Größe | +| Dateigrößen-Check | **4 MB** (Excalidraw) + **4 MB** (Whiteboard) | Größere Bilder werden abgelehnt | +| RAM-Verbrauch (Base64 im JSON) | **~50 MB/Bild** (4000×3000px decoded) | Ab ~3 Bildern → Chrome OOM-Crash | + +Alle diese Limits sind **doppelt** vorhanden: einmal im Excalidraw-Core (`percentages-*.chunk.mjs`), einmal in der Whiteboard-Bridge (`NcSelect-*.chunk.mjs`). + +--- + +## 2. Architektur: Warum kein Docker-Neustart nötig ist + +``` +Docker Image (read-only) Docker Volume (read-write) +┌──────────────────────────┐ ┌─────────────────────────────┐ +│ Nextcloud PHP │ │ /custom_apps/whiteboard/js/ │ +│ Apache/Nginx Config │ │ ├─ NcSelect-CknHatsl.mjs │ ← Patches #3–#6 +│ System-Libs │ │ ├─ percentages-*.mjs │ ← Patches #1–#2 +│ │ │ └─ whiteboard-*-patch.js │ ← Scroll + Quality +│ (NIE angefasst) │ │ │ +└──────────────────────────┘ └─────────────────────────────┘ + ▲ + Persistiert über Restarts, + NICHT über App-Updates +``` + +**Der gesamte Bildverarbeitungs-Pipeline läuft im Browser:** +1. User dropped Bild → Whiteboard JS (`B0()` mit `image-blob-reduce`) +2. → Excalidraw JS (`initializeImage()`) +3. → Canvas-Rendering + Base64-Encode +4. → Upload JSON an Nextcloud (PHP macht **nichts** mit dem Bild) + +Deshalb reicht ein **Browser-Cache-Refresh (Cachebuster)** — kein Container-Neustart. + +--- + +## 3. Alle Patches (Stand 2026-07-02) + +### 3.1 Scroll-Zoom (Miro-like) +**Datei:** `scripts/whiteboard-scroll-patch.js` +- Scrollen = Zoomen, Mittelklick-Drag = Pannen +- Registriert in `LoadViewerListener.php`, `WhiteboardDirectEditor.php`, `RecordingController.php` + +### 3.2 Image Quality (Post-Processing) +**Datei:** `scripts/whiteboard-image-patch.js` +- Verhindert Qualitätsverlust durch Excalidraws internes `imageBlobReduce`-Postprocessing + +### 3.3 `IM` und `TM` — Whiteboard-eigene Limits +**Datei:** `percentages-BXMCSKIN-D6x-nnv3.chunk.mjs` + +``` +...Cl=2,Ff=[1,2,3],dl=10,IM=1/0,TM=100*1024*1024,je=... +``` + +| Konstante | Vorher | Nachher | Bedeutung | +|---|---|---|---| +| `IM` | `1440` | `1/0` (=∞) | Max. Kantenlänge für `image-blob-reduce` | +| `TM` | `4*1024*1024` | `100*1024*1024` | Max. Dateigröße für Whiteboard-Preprocessing | + +### 3.4 `maxImageSizeBytes` — Excalidraw-Fallback +**Datei:** `NcSelect-CknHatsl.chunk.mjs` + +``` +maxImageSizeBytes:F||9e15 +``` + +| Vorher | Nachher | +|---|---| +| `F` (Nextcloud Config, `undefined` → 4MB Hardcoded) | `F\|\|9e15` (Fallback ≈ unendlich) | + +### 3.5 `maxWidthOrHeight` + `imageOptions` — Excalidraw-Limits +**Datei:** `NcSelect-CknHatsl.chunk.mjs` + +``` +maxWidthOrHeight:8192,imageOptions:{maxWidthOrHeight:1/0,maxFileSizeBytes:1/0} +``` + +| Property | Vorher | Nachher | +|---|---|---| +| `maxWidthOrHeight` | `2048` (harter Reject) | `8192` | +| `imageOptions.maxWidthOrHeight` | `1440` (Downscale) | `1/0` (=∞) | +| `imageOptions.maxFileSizeBytes` | `4*1024*1024` (fileTooBig) | `1/0` (=∞) | + +### 3.6 Infrastruktur +| Ebene | Einstellung | Wert | +|---|---|---| +| Nextcloud DB | `occ config whiteboard maxFileSize` | `100` (MB) | +| PHP | `upload_max_filesize` | `512M` | +| PHP | `post_max_size` | `512M` | +| PHP | `memory_limit` | `512M` | +| Nginx | `client_max_body_size` | `0` (unlimited) | +| Nginx | Whiteboard JS/CSS `Cache-Control` | `no-cache, no-store, must-revalidate` | +| Cache | `theming cachebuster` | **26** | + +--- + +## 4. Warum das nicht reicht (OOM-Problem) + +Mit `IM=1/0` und `imageOptions.maxWidthOrHeight=1/0` wird kein Downscaling mehr gemacht. Das bedeutet aber: **Jedes importierte Bild wird in voller原生auflösung als Base64 im Excalidraw-JSON gespeichert.** + +**RAM-Verbrauch pro Bild:** +``` +Breite × Höhe × 4 Bytes (RGBA decoded) + Base64-String (~33% Overhead) + +5000 × 2000 × 4 = 40 MB decoded + 13 MB Base64 = ~53 MB RAM/Bild +4000 × 3000 × 4 = 48 MB decoded + 16 MB Base64 = ~64 MB RAM/Bild +``` + +**Bei 3 Bildern → ~160 MB + UI-Overhead → OOM-Crash nach ~2 Minuten.** + +--- + +## 5. Excalidraw vs. Miro: Der fundamentale Unterschied + +| | Excalidraw/Whiteboard | Miro | +|---|---|---| +| Bild-Speicherung | **Base64 inline im JSON** | URL-Referenz auf Server | +| Rendering | Canvas 2D, alle Bilder immer im RAM | WebGL + Tiled-Streaming | +| LOD | ❌ Nicht vorhanden | ✅ Nur sichtbare Kacheln geladen | +| RAM pro 4K-Bild | ~50 MB | ~0 MB (lazy, GPU-Textur) | +| 50× 4K-Bilder | 💥 unmöglich | ✅ problemlos | + +Miro speichert nur **URLs** (100 Bytes statt 50 MB) und lädt Bilder erst beim Hinscrollen. + +--- + +## 6. Geplanter Fix: URL-Referenzen statt Base64 + +### Ansatz +Statt das Bild als `data:image/png;base64,...` im JSON zu speichern, soll nur eine **URL** gespeichert werden. Der Browser lädt das Bild dann nur bei Bedarf. + +``` +// JETZT: +"files": { "abc123": { "dataURL": "data:image/png;base64,iVBORw0K...", ... } } + +// ZIEL: +"files": { "abc123": { "url": "/remote.php/dav/files/.../image.png", ... } } +``` + +### Was geändert werden muss + +| Komponente | Änderung | Aufwand | +|---|---|---| +| **Whiteboard Import** | Bild als Nextcloud-Datei speichern (WebDAV), nicht als Base64 | JS-Patch | +| **Excalidraw JSON-Format** | `dataURL` → `url` (relativer Pfad) | JS-Patch | +| **Excalidraw Rendering** | `Image()` vom Server laden statt aus Base64 | JS-Patch | +| **Nextcloud CORS** | `Access-Control-Allow-Origin` für Bild-URLs | Config | +| **File-ID Mapping** | `fileId` → Nextcloud-Dateipfad | JS-Patch | + +### Risiken +- Alles JS-Patches auf dem Volume → gehen bei App-Update verloren +- Startup-Script zur Wiederherstellung nötig +- Langfristig: Upstream-PR an Excalidraw/Whiteboard + +--- + +## 7. Direkter Link zum Excalidraw-Upstream + +- **DEFAULT_IMAGE_OPTIONS** (Source of Truth): [`packages/common/src/constants.ts#L340-L342`](https://github.com/excalidraw/excalidraw/blob/master/packages/common/src/constants.ts) +- **LOD Feature Request** (seit 2022 offen): [Issue #5334](https://github.com/excalidraw/excalidraw/issues/5334) +- **Image Resolution Bug**: [Issue #5334](https://github.com/excalidraw/excalidraw/issues/5334) + +--- + +## 8. Verifikation + +Server-seitig verifizieren (nach jedem Patch): +```bash +# NcSelect +curl -s "https://nextcloud.niklashmotion.art/custom_apps/whiteboard/js/NcSelect-CknHatsl.chunk.mjs?v=26" \ + | grep -oP 'maxImageSizeBytes[^,]+|imageOptions:\{[^}]+\}' + +# Percentages +curl -s "https://nextcloud.niklashmotion.art/custom_apps/whiteboard/js/percentages-BXMCSKIN-D6x-nnv3.chunk.mjs?v=26" \ + | grep -oP 'IM=[^,]+|TM=[^,]+' +``` + +Automatisiert: `scripts/verify-whiteboard-patches.sh` (im Skill-Verzeichnis). + +--- + +## 9. Nächste Schritte + +1. [x] Alle Limits beseitigt (6 JS-Patches + Infrastruktur) +2. [ ] **4096px-Cap setzen** (Quick-Fix gegen OOM) — 2 Minuten, 12+ Bilder stabil +3. [ ] **URL-Referenzen implementieren** (echte Lösung) — 1–2 Tage Entwicklungszeit +4. [ ] Startup-Script für Persistenz über App-Updates +5. [ ] Langfristig: Upstream-PR für konfigurierbare Limits + URL-Referenzen diff --git a/docs/02-implementierungsplan.md b/docs/02-implementierungsplan.md new file mode 100644 index 0000000..0fe2d29 --- /dev/null +++ b/docs/02-implementierungsplan.md @@ -0,0 +1,406 @@ +# 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), // ← 1–3 (sichtbare) +> thumbnailCount: countWhere(files, f => !f._fullImageLoaded), // ← 7–9 +> }; +> // 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×: 1–3 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 | 2–4 h | 7 Checks | +| Phase 3: Rendering/LOD | 4–8 h | 6 Checks | +| Phase 4: Migration + Stress | 1–2 h | 6 Checks | +| **Total** | **1–2 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` | diff --git a/test-scripts/generate-test-images.sh b/test-scripts/generate-test-images.sh new file mode 100755 index 0000000..975050a --- /dev/null +++ b/test-scripts/generate-test-images.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Generiert PNG-Testbilder mit exakten Dimensionen auf orange-desktop +# Aufruf: bash generate-test-images.sh + +HOST="${1:-orange@niklashmotion.art}" + +ssh "$HOST" "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') +\"" +echo "Done. Test images on $HOST:/tmp/test-*.png" diff --git a/test-scripts/verify-phase1.sh b/test-scripts/verify-phase1.sh new file mode 100755 index 0000000..e149917 --- /dev/null +++ b/test-scripts/verify-phase1.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Verifiziert Phase 1: CORS + WebDAV-Zugriff +# Aufruf: bash verify-phase1.sh + +set -e +source ~/.hermes/nextcloud.env 2>/dev/null || source /opt/data/home/.hermes/nextcloud.env + +echo "=== Phase 1 Verification ===" +echo "" + +# Check 1: Ordner existiert +echo "1. WhiteboardAssets folder..." +CODE=$(curl -s -o /dev/null -w "%{http_code}" -u "$NEXTCLOUD_USER:$NEXTCLOUD_TOKEN" \ + -X PROPFIND "$NEXTCLOUD_URL/remote.php/dav/files/$NEXTCLOUD_USER/WhiteboardAssets/") +if [ "$CODE" = "207" ]; then echo " ✅ Folder exists (HTTP $CODE)"; else echo " ❌ HTTP $CODE"; fi + +# Check 2: CORS-Header +echo "2. CORS headers..." +CORS=$(curl -sI "https://nextcloud.niklashmotion.art/remote.php/dav/files/Hermes/WhiteboardAssets/test.png" 2>/dev/null | grep -i "access-control-allow-origin" || echo "") +if [ -n "$CORS" ]; then echo " ✅ $CORS"; else echo " ❌ No CORS header"; fi + +# Check 3: Upload +echo "3. Upload test..." +ssh orange@niklashmotion.art "test -f /tmp/test-small-500x400.png && echo 'exists' || echo 'missing'" 2>/dev/null +echo " (run generate-test-images.sh first if missing)" + +# Check 4: All 6 existing patches still active +echo "4. Existing patches..." +ssh orange@niklashmotion.art "bash /opt/data/skills/devops/whiteboard-patches/scripts/verify-whiteboard-patches.sh" 2>/dev/null + +echo "" +echo "=== Done ==="