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:
paste-Event +clipboardData.items+getAsFile()(synchron, kein Permission-Prompt — die NutzergesteCmd+Vgilt als implizite Zustimmung). Überitemsiterieren, das ersteitem.type === 'image/png'nehmen,getAsFile()→ Blob. Funktioniert in Chrome und Safari (≥13.1).- Async-Clipboard-API
navigator.clipboard.read()als Knopf-Variante („Aus Zwischenablage einfügen"-Button) — liefertClipboardItemmit.typesund.getType('image/png'). Vorteil: man kann gezieltimage/pngverlangen. Nachteil: kann in manchen Browsern einen Permission-Prompt auslösen und ist an eine echte Nutzergeste gebunden.
Praktisch: beide kombinieren — paste-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
- Same-origin = der einfachste, sicherste Auth-Weg überhaupt: das vorhandene
ss_session-Cookie reicht,SameSite=Laxbleibt unangetastet, kein CORS, kein Preflight, kein Secret. - Der Endpoint sitzt automatisch hinter der Middleware (
auth_guardinmain.pylässt nur/api/auth/*ohne Session durch) — Pocket-ID schützt ihn, ohne dass wir etwas Neues bauen. - Die Doku-Archiv-Variante bleibt lesend erhalten (öffentliche Vorschau), wird aber nicht zum Schreib-Vektor.
- Bestehender Bonus: Cron/Worker können denselben Endpoint per
X-Internal-Tokenbedienen, falls man je serverseitig Funde injizieren will.
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:
- offen (leer) — wie bisher.
- ✓ ok — grüner Haken (
--kelp), wie bisher (localStorage). - ✗ 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)
- Optimistisch + Queue: Notiz/Defekt sofort in localStorage
(
ss-testplan-w7-feedback), Bilder als Base64/Blob ebenfalls zwischenpuffern. - Auto-Send (debounced ~1,5 s nach letzter Änderung und beim Klick auf
„Senden"):
POSTan den Endpoint. Erfolg → Queue-Eintrag als „synced" markieren, grüne Quittung. Fehler/Offline → bleibt in Queue, Re-Try beim nächsten Laden/online-Event. Nichts geht verloren, auch wenn die Verbindung wackelt oder der Tab geschlossen wird. - Idempotenz: jeder Defect-Eintrag bekommt eine stabile
client_id(welle-punkt-uuid); der Server überschreibt denselben Eintrag statt zu duplizieren (Re-Sends sind harmlos). - Der bestehende localStorage-Haken-Mechanismus bleibt unverändert — der Testplan ist weiter offline-tauglich; nur die Defect-Daten gehen zum Server, sobald online.
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):
- Nur
welle-{int}als Pfad-Segment (regex^\d+$) → kein Path-Traversal. - Content-Type-Whitelist
image/png,image/jpeg; alles andere 415. - Größenlimit pro Bild (z. B. 8 MB) + max. Bilder pro Request (z. B. 20) → blockt versehentliches TIFF / Disk-Flooding.
- Dateinamen serverseitig vergeben (
<client_id>-<index>.png), nie den Client-Namen übernehmen. - Optional: simpler Rate-Limit (z. B. via vorhandener Session-Bindung — reicht, weil hinter Auth).
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.json → findings.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.
- Endpoint + Ablage (
app/backend/main.py) —POST /api/testplan/welle-{n}/feedback: multipart parsen, Limits/Whitelist, Bilder +findings.json(merge perclient_id) nachapp/data/testplan-feedback/welle-{n}/schreiben. ~1–1,5 h. - Static-Serving der Testpläne unter Auth — Route
GET /testplan/welle-{n}liefert die HTML (entwederapp/data/testplan/…oder direkt ausdocs/content/testplaene/gemountet). Liegt automatisch hinterauth_guard. ~0,5 h. - 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). - MD-Zusammenfassung (optional) —
findings.json → findings.md-Generator. ~0,5 h. - 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 wirdKEY = '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)
- 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.) - MD-Zusammenfassung automatisch oder auf Ansage? Sollen Funde nach einer
Welle automatisch als
findings.mdins ö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.) - 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):
- 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 inapp/testplaene/(ro-Mount in den Container — neue Welle = HTML ablegen, kein Rebuild). - 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"). - 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).