Zuletzt aktualisiert:

Defect-Erfassung direkt im Testplan (Paste-and-go statt AirDrop-Umweg)

Tiefenrecherche + Konzept · 07.06.2026 · für Dennis (Silverscale Family Hub)

Frage: Wie meldet Dennis Funde aus dem iPhone-Gerätetest direkt am interaktiven Testplan (pro Checklisten-Punkt: ✓ ODER ✗-Defect mit Notiz + Screenshot/s) — und zwar so, dass (a) der iPhone-Screenshot per Universal Clipboard mit einem simplen Cmd+V in die Testplan-Seite am Mac wandert und (b) Funde + Bilder ohne Umweg als Dateien auf dem VPS landen, wo die nächste Claude-Session sie liest („lies das Feedback zu Welle 7")?

Heute nervt: Screenshot → AirDrop → Cyberduck/SFTP → Pfad nicht parat → kein @-Verweis. Funde nur als Freitext in die Session getippt.


1. Executive Summary — Empfehlung

Empfehlung: Option (b) + (a). Die Testpläne künftig zusätzlich hinter der App-Auth unter https://silverscale.dennisfisch.de/testplan/welle-N ausliefern (static, vom FastAPI-Backend) und einen Feedback-Endpoint in derselben App-API betreiben (POST /api/testplan/<welle>/feedback). Damit ist die Testplan-Seite same-origin mit der API → kein CORS, der bestehende 180-Tage-Session-Cookie (Dennis ist am Mac eh eingeloggt) trägt die Auth, kein zweites Geheimnis im HTML, und der Endpoint sitzt automatisch hinter Pocket-ID (ADR-16). Die Funde landen als app/data/testplan-feedback/welle-N/findings.json + img/*.png — genau die vorhersagbare Ablage, die eine nächste Session direkt liest.

Der Screenshot-Paste funktioniert: Universal Clipboard legt den iPhone-Screenshot in die Mac-Zwischenablage, und ein paste-Event im Browser liefert das Bild — aber nur über clipboardData.items + getAsFile() bzw. die Async-Clipboard-API, NICHT zuverlässig über clipboardData.files (in Safari ist files bei Screenshots oft leer). Dazu die TIFF-Falle: die Zwischenablage bietet PNG und TIFF an — wir müssen gezielt image/png greifen, sonst lädt man ein riesiges TIFF hoch. Beides ist mit wenigen Zeilen JS lösbar; Drag&Drop aufs Fenster bleibt als Fallback.

Das öffentliche Doku-Archiv (silverscale-docs.dennisfisch.de, nginx, keine Auth) bleibt der Ort für die Vorschau / das Lesen der Testpläne, bekommt aber keinen Upload-Endpoint (öffentliches Internet, ufw inaktiv — kein Schreibpfad ohne Auth). Optional rendern wir nach jeder Welle eine Markdown-Zusammenfassung der Funde und legen sie regulär ins Archiv (docs/content/dev/), damit der Stand auch dort sichtbar ist.


2. In einfachen Worten

Stell dir vor, neben jedem Häkchen im Testplan sitzt ein kleines „🐞 Defect". Tippst du es an, klappt ein Notizfeld auf und eine Fläche „hier Screenshot einfügen". Du machst auf dem iPhone einen Screenshot, tippst „Kopieren" (oder er liegt eh per Universal Clipboard bereit), gehst an den Mac, klickst in die Fläche und drückst Cmd+V — Bild ist drin, als kleines Vorschaubildchen. Du kannst mehrere reinpacken. Ein Klick auf „Senden", fertig — grüner Haken „gespeichert".

Im Hintergrund schickt die Seite Notiz + Bilder an deine eigene App (die dich am Mac schon kennt, du bist eingeloggt). Die App legt alles als Dateien auf den VPS, in einen festen Ordner pro Welle. Beim nächsten Mal sagst du Claude einfach „lies das Feedback zu Welle 7" — und ich öffne den Ordner direkt, mit Bildern und allem. Kein AirDrop, kein Cyberduck, kein Pfad-Suchen mehr.

Wichtig dabei: Damit „du bist eingeloggt" gilt, muss die Testplan-Seite auf deiner App-Adresse wohnen (silverscale.dennisfisch.de/testplan/…), nicht auf der öffentlichen Doku-Adresse. Auf der Doku-Adresse gibt es keinen Login — da dürfte jeder im Internet hochladen.


3. Continuity-/Paste-Recherche — funktioniert Cmd+V?

Kurz: Ja, mit der richtigen API. Universal Clipboard (Continuity) schreibt den iPhone-Screenshot in die systemweite Mac-Zwischenablage (NSPasteboard); für den Browser ist das danach nicht mehr von einem lokalen Mac-Screenshot zu unterscheiden. Voraussetzungen für Universal Clipboard selbst: gleiche Apple-ID, Bluetooth + WLAN + Handoff an, Geräte nah beieinander. (Praxis-Falle: Universal Clipboard hat ~1–2 s Latenz und scheitert gelegentlich an Handoff/BLE — dann hilft der Drag&Drop-Fallback bzw. ein zweiter Versuch.)

3.1 Die zentrale Falle: clipboardData.files ist in Safari oft LEER

Bei einem Screenshot in Safari meldet der paste-Event über clipboardData.types korrekt ["public.png","image/png","public.tiff","image/tiff"]aber clipboardData.files hat Länge 0. Verlässt man sich nur auf .files (wie viele simple „paste image"-Snippets), passiert in Safari schlicht nichts.

Zwei robuste Wege, in dieser Reihenfolge:

  1. paste-Event + clipboardData.items + getAsFile() (synchron, kein Permission-Prompt — die Nutzergeste Cmd+V gilt als implizite Zustimmung). Über items iterieren, das erste item.type === 'image/png' nehmen, getAsFile() → Blob. Funktioniert in Chrome und Safari (≥13.1).
  2. Async-Clipboard-API navigator.clipboard.read() als Knopf-Variante („Aus Zwischenablage einfügen"-Button) — liefert ClipboardItem mit .types und .getType('image/png'). Vorteil: man kann gezielt image/png verlangen. Nachteil: kann in manchen Browsern einen Permission-Prompt auslösen und ist an eine echte Nutzergeste gebunden.

Praktisch: beide kombinierenpaste-Listener als Hauptweg, optional ein „Einfügen"-Knopf, der navigator.clipboard.read() versucht.

3.2 Die TIFF-Falle (Safari/macOS)

macOS legt Bilder in der Zwischenablage doppelt ab: image/png und image/tiff (public.tiff). TIFF ist unkomprimiert → ein iPhone-Screenshot wird als TIFF schnell 10–30 MB, als PNG nur ~0,3–1,5 MB.

Niemals „erstes image/*-Item" nehmen. Immer gezielt image/png bevorzugen; nur wenn kein PNG da ist, auf image/jpeg ausweichen, TIFF ignorieren oder clientseitig neu kodieren. Sicherste Absicherung: das Blob über ein <img> + Offscreen-<canvas> + canvas.toBlob(cb,'image/png',0.9) neu als PNG (oder JPEG) enkodieren — das normalisiert Format und Größe in einem Schritt und killt jedes versehentliche TIFF.

3.3 Chrome auf macOS

Chrome füllt clipboardData.files bei Screenshots häufiger korrekt als Safari, bietet aber dieselben Typen an. Der items/getAsFile()-Weg deckt Chrome und Safari gemeinsam ab — also einheitlich über items gehen, files höchstens als zusätzlichen Fallback prüfen.

3.4 Fallback: Drag&Drop aufs Fenster

Wenn Paste klemmt (Universal-Clipboard-Aussetzer, Permission verweigert): Bild aus Vorschau/Finder/Fotos per Drag&Drop auf die Defect-Zone ziehen. Das drop-Event liefert e.dataTransfer.files zuverlässig in beiden Browsern. Dazu ein klassischer „Datei wählen"-<input type=file accept=image/* multiple> als letzter Anker (öffnet auf dem Mac den Datei-Dialog, auf dem iPhone direkt Kamera/Fotos — nett, falls Dennis den Testplan doch mal am iPhone offen hat).

3.5 Verdikt Tabelle

Mechanismus Chrome/macOS Safari/macOS Permission-Prompt TIFF-Risiko
clipboardData.files meist ok oft leer nein mittel
clipboardData.items+getAsFile() ok ok nein nur wenn man image/png nicht filtert
navigator.clipboard.read() ok ok (≥13.1) ggf. ja gezielt vermeidbar (getType('image/png'))
Drag&Drop (dataTransfer.files) ok ok nein niedrig (Datei hat echten Typ)
<input type=file> ok ok nein niedrig

Empfohlener Code-Pfad: paste-Listener → über items iterieren → image/png bevorzugen → Blob über Canvas zu PNG normalisieren → Thumbnail + in Sende-Queue. Drag&Drop + File-Input als sichtbare Fallbacks.


4. Architektur-Optionen (a–d) — Security & Aufwand

Anforderung: Schreibpfad vom Testplan zum VPS-Dateisystem, ohne ein neues Loch in die Sicherheit zu reißen. Randbedingungen: Doku-Archiv ist öffentlich + ohne Auth; App ist komplett hinter Pocket-ID (Cookie 180 Tage, backend/auth.py); ufw inaktiv, nur Traefik 80/443 lauscht; Worker nutzen X-Internal-Token.

# Option Auth-Modell CORS Security-Bewertung Aufwand Verdikt
a Feedback-Endpoint in App-API + Testplan bleibt auf Doku-Domain + CORS credentials App-Session-Cookie, aber cross-origin Muss CORS mit allow_credentials=True + exakter Origin der Doku-Domain öffnen; SameSite=Lax-Cookie wird bei cross-site-fetch NICHT mitgeschickt → bräuchte SameSite=None; Secure (schwächt CSRF-Schutz appweit!) mittel Nein — zwingt das Session-Cookie auf SameSite=None für die ganze App, nur damit eine fremde Origin posten darf. Schlechtes Kosten/Nutzen.
b Testpläne zusätzlich auf App-Domain hinter Auth (/testplan/welle-N) + Endpoint same-origin App-Session-Cookie, same-origin Kein CORS nötig Sauber: liegt komplett hinter Pocket-ID (ADR-16), Cookie bleibt SameSite=Lax, kein Geheimnis im HTML, kein zweiter Origin. Einziger „Preis": Testplan-Vorschau ist dann auth-pflichtig (für Dennis egal, er ist eingeloggt). niedrig–mittel JA — Empfehlung.
c Mini-Endpoint mit Token im HTML (Bearer/Query) auf eigener Subdomain ohne Cookie-Auth statisches Shared Secret im Testplan-Quelltext egal (Token statt Cookie) Riskant: Token steht im öffentlich erreichbaren HTML → wer die URL kennt, kann schreiben (Spam/Müll-Uploads, Disk-Füllung). ufw aus → nur Obfuskation. Nur vertretbar mit Rate-Limit + Größenlimit + Wegwerf-Token pro Welle. niedrig Nein (außer als Notnagel), Klartext-Secret im öffentlichen HTML ist genau das, was wir laut CLAUDE.md vermeiden.
d here.now-Drive / externer Upload-Dienst dessen Auth Daten landen außerhalb des VPS, Claude müsste sie erst zurückholen — widerspricht „direkt im Dateisystem lesbar". niedrig Nein — bricht das Kernziel (Dateien lokal beim Code).

Warum (b) gewinnt

Hinweis zum Frontend-Link-Interceptor: Das App-Frontend zwingt jeden Cross-Origin-Link ins Safari-Sheet (initExternalLinks, siehe Recherche „Doku-Hosting"). /testplan/… ist same-origin → in der iOS-App würde es im WebView bleiben. Für den Defect-Flow ist das egal (Dennis nutzt den Testplan am Mac-Browser), aber es ist ein weiterer Pluspunkt für (b).


5. UX-Konzept am Testplan

Bleibt im bestehenden Look — der Testplan hat schon alle nötigen CSS-Variablen (--coral für Defects, --kelp für „erledigt", --sun für Warnung, --card, --hair, --ink*). Kein neues Designsystem, nur drei Zustände pro Punkt.

5.1 Drei Zustände pro Checklisten-Punkt

Heute: nur Checkbox (✓ / leer). Neu: jeder label.item bekommt rechts eine kleine 🐞-Defect-Taste. Zustände:

  1. offen (leer) — wie bisher.
  2. ✓ ok — grüner Haken (--kelp), wie bisher (localStorage).
  3. ✗ Defect — Punkt klappt einen Defect-Block auf (Rahmen in --coral, analog zur bestehenden .melden-Box-Optik): - Notiz-Textarea („Was ist passiert?") — autosave nach localStorage. - Paste-Zone: gestrichelter Rahmen, Text „Screenshot hier einfügen (Cmd+V) oder herziehen" + kleiner „📎 Datei wählen"-Button. - Thumbnail-Liste: jedes eingefügte Bild als ~64px-Vorschau mit ✕ zum Entfernen; Mehrfach-Bilder erlaubt. - Senden-Status: pro Punkt eine Zeile „gespeichert ✓ 12:04" / „wird gesendet…" / „offline — in Warteschlange (2)".

Defect ↔ ok sind exklusiv; ein Punkt mit Defect zählt im Fortschrittsbalken als „behandelt" (Balken misst „bearbeitet", nicht „heil") — Defects bekommen zusätzlich einen kleinen Coral-Counter im Section-Header, parallel zum bestehenden .count-Zähler.

5.2 Senden-Logik (verlustfrei)

5.3 Bild-Aufbereitung im Client (vor Upload)

PNG aus der Zwischenablage durch Canvas normalisieren: lange Kante auf max. ~1600 px herunterskalieren, als PNG oder JPEG q0.85 re-enkodieren. Spart Bandbreite, killt TIFF, und iPhone-Screenshots bleiben gut lesbar. Ergebnis-Blob → FormData.


6. Datenfluss & Ablage

6.1 Endpoint-Skizze (FastAPI, app/backend/main.py)

GET  /testplan/welle-{n}          → liefert die statische Testplan-HTML
                                     (StaticFiles oder FileResponse; hinter Auth)
POST /api/testplan/welle-{n}/feedback
     Body: multipart/form-data
       findings : JSON-String  [ {client_id, item_id, item_text, note, ts,
                                   image_names:[...] }, ... ]
       img       : eine oder mehrere Bilddateien (image/png|jpeg)
     Auth      : App-Session-Cookie (same-origin) ODER X-Internal-Token
     Antwort   : { ok:true, saved:<n>, dir:"app/data/testplan-feedback/welle-7" }

Bild-Handling / Limits (serverseitig hart durchsetzen, nicht dem Client trauen):

6.2 Ablage auf dem VPS (stabil, vorhersagbar)

app/data/testplan-feedback/
  welle-7/
    findings.json      ← Liste aller Defects der Welle (Server merged per client_id)
    findings.md        ← (optional) menschenlesbare Zusammenfassung, autogeneriert
    img/
      <client_id>-0.png
      <client_id>-1.png

findings.json (Beispiel-Eintrag):

{
  "client_id": "w7-c12-9f3a",
  "item_id": "c12",
  "item_text": "Stepper „−" fragt nach ¼ · ½ · Rest …",
  "note": "Sheet öffnet, aber „Rest“ zieht 0 g ab — Balken bleibt voll.",
  "ts": "2026-06-08T12:04:11+02:00",
  "images": ["img/w7-c12-9f3a-0.png", "img/w7-c12-9f3a-1.png"]
}

app/data/ liegt bereits im 03:30-Backup (CLAUDE.md) → Funde sind mitgesichert.

6.3 Wie Claude es liest

Stabiler, ansagbarer Pfad: „lies app/data/testplan-feedback/welle-7/" → Session öffnet findings.json (Struktur), liest note + item_text je Defect und betrachtet die Bilder mit dem Read-Tool (PNG wird visuell dargestellt). Kein AirDrop, kein Pfad-Raten, direkt @app/data/testplan-feedback/welle-7/img/… referenzierbar.

6.4 Optionale Markdown-Zusammenfassung fürs Archiv

Ein kleiner Generator (Teil des Endpoints oder ein Mini-Skript app/testplan-feedback-md.py) rendert findings.jsonfindings.md („## Welle 7 — N Defects" + je Defect Überschrift, Notiz, eingebettete Bilder). Diese MD kann nach docs/content/dev/welle-7-funde.md kopiert und mit python3 docs/build.py + docker restart silverscale-docs ins öffentliche Archiv gehoben werden — so wird der Welle-Stand auch dort sichtbar (Bilder via copy_sibling_assets, das img/-Geschwisterordner schon mitkopiert, siehe docs/build.py). Manuell auf Ansage (Archiv ist öffentlich — vorher prüfen, dass keine sensiblen Screenshots dabei sind).


7. Implementierungsplan (mit Aufwand)

Reihenfolge so, dass nach Schritt 3 schon der volle Flow für eine Welle steht.

  1. Endpoint + Ablage (app/backend/main.py) — POST /api/testplan/welle-{n}/feedback: multipart parsen, Limits/Whitelist, Bilder + findings.json (merge per client_id) nach app/data/testplan-feedback/welle-{n}/ schreiben. ~1–1,5 h.
  2. Static-Serving der Testpläne unter Auth — Route GET /testplan/welle-{n} liefert die HTML (entweder app/data/testplan/… oder direkt aus docs/content/testplaene/ gemountet). Liegt automatisch hinter auth_guard. ~0,5 h.
  3. Defect-UI-Block im Testplan-Template — 🐞-Taste pro Item, Defect-Block (Notiz, Paste-Zone, Thumbnails), CSS in den vorhandenen Variablen, Paste-/Drop-/File-Handler mit items+getAsFile()+PNG-Filter+Canvas-Resize, localStorage-Queue + Auto-Send + Re-Try. ~2–3 h (das Herzstück).
  4. MD-Zusammenfassung (optional) — findings.json → findings.md-Generator. ~0,5 h.
  5. In den Welle-Workflow übernehmen — den Defect-Block in das Testplan-Template ziehen, aus dem jede Welle entsteht (heute sind die Wellen handgeschriebene HTML in docs/content/testplaene/welle-N.html; pro Welle wird KEY = 'ss-testplan-wN' und die Welle-Nummer gesetzt). Ein gemeinsames <script>/<style>-Snippet bzw. ein winziger Template-Generator stellt sicher, dass jede neue Welle das Defect-Feature automatisch erbt — nur Welle-Nummer + Checklisten-Inhalt sind dann noch manuell. ~0,5–1 h (einmalig).

Gesamt: ~5–7 h. Nichts davon berührt App-Datenmodell oder bestehende Endpoints (eigener Pfad, eigene Datei-Ablage) — also auch unter der Ein-Schreiber-Regel risikoarm nachrüstbar.

Doppelte Verfügbarkeit (Archiv-Vorschau bleibt)

Der Testplan existiert dann an zwei Orten: weiterhin im öffentlichen Archiv (silverscale-docs…/testplaene/welle-N.html, nur lesen/abhaken, keine Sende-Funktion bzw. Sende läuft ins Leere/zeigt „nur Vorschau") und hinter Auth auf der App-Domain (/testplan/welle-N, voller Defect-Flow). Sauberste Variante: die Sende-Logik prüft location.origin — auf der Doku-Domain blendet sie die 🐞-Tasten aus oder zeigt einen Hinweis „Funde melden: Testplan über die App öffnen". So bleibt das öffentliche Archiv frei von jedem Schreibvektor.


8. Offene Fragen an Dennis (max. 3)

  1. Ein Ort oder zwei? Soll der Testplan künftig nur noch auf der App-Domain (/testplan/welle-N, hinter Login) leben — oder weiter zusätzlich im öffentlichen Archiv als reine Lese-/Abhak-Vorschau (dann ohne Sende-Funktion dort)? (Empfehlung: beides, App-Domain als Defect-Weg.)
  2. MD-Zusammenfassung automatisch oder auf Ansage? Sollen Funde nach einer Welle automatisch als findings.md ins öffentliche Doku-Archiv wandern (Achtung: Screenshots sind dann öffentlich!) — oder bleibt das ein manueller „push, wenn ich's sage"-Schritt? (Empfehlung: manuell, wegen öffentlicher Sichtbarkeit.)
  3. Bild-Obergrenze ok? Reichen ~1600 px lange Kante / PNG-q0.85 / max. 8 MB pro Bild — oder willst du Originalauflösung behalten (größere Uploads/Disk)?

9. Nachtrag: Entscheidung & Umsetzung (07.06.2026 spätnachts)

Dennis hat entschieden, das System ist gebaut (ADR-50):

  1. Nur EIN Ort: Testpläne leben ausschließlich hinter Login auf https://silverscale.dennisfisch.de/testplan (Index) bzw. /testplan/welle-N. Die Archiv-Kategorie „Testpläne" wurde entfernt; Quellen liegen jetzt in app/testplaene/ (ro-Mount in den Container — neue Welle = HTML ablegen, kein Rebuild).
  2. findings.md bleibt LOKAL (app/data/testplan-feedback/welle-N/) — kein Auto-Push ins öffentliche Archiv; die Claude-Session rendert die Funde direkt („lies das Welle-N-Feedback").
  3. Bilder UNSKALIERT: kein Canvas-Resize (Artefakt-Vermeidung); nur Nicht-PNG/JPEG (TIFF-Falle) wird verlustfrei nach PNG umkodiert. Limit 15 MB/Bild, Magic-Byte-Prüfung serverseitig.

Implementiert wie in §6 skizziert: GET /testplan(/{plan}) · POST /api/testplan/{plan}/img (Raw-Body) · POST /api/testplan/{plan}/feedback (Upsert je item_id, schreibt findings.json + findings.md). Defect-UI als generischer Template-Block in welle-7.html (🐞 je Punkt, Paste über clipboardData.items mit image/png-Vorrang, Drag&Drop + 📎-Fallback, localStorage-Persistenz, Auto-Save debounced, Re-Sync bei online).