Initial: Projektstruktur, Referenz-Dokument, Implementierungsplan, Test-Skripte
This commit is contained in:
@@ -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
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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` |
|
||||||
Executable
+29
@@ -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"
|
||||||
Executable
+32
@@ -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 ==="
|
||||||
Reference in New Issue
Block a user