# Problemmelder — Widget-Integration für interne Apps

Diese Anleitung richtet sich an Entwickler **und KI-Assistenten**, die das
Problemmelder-Widget in eine interne App einbauen sollen. Das Widget ist ein
schwebender 🐛-Button, über den Nutzer direkt aus der App heraus einen **Bug
oder eine Idee melden** – ein Textfeld, keine Kategorie-Auswahl. Die Meldung wird **als Issue im
Gitea-Repo des Projekts** (git.tradeo.de) angelegt – inklusive Screenshot,
Anhängen, aktueller URL, Browser-Info und dem Console-Log.

Die App muss dafür nichts speichern und keine Zugangsdaten kennen. Sie bindet
ein Script ein, mehr nicht.

## Eckdaten

| | |
| --- | --- |
| Script | `https://problem.tradeo.dev/widget.js` |
| API (vom Widget genutzt) | `POST https://problem.tradeo.dev/api/v1/widget/reports` |
| Status-Check | `GET https://problem.tradeo.dev/api/v1/widget/status` → `{ "enabled": true, "gitea": { "owner": "…", "extraOwners": ["…"] } }` |
| Ziel | Issue in `git.tradeo.de/<owner>/<data-project>` bzw. `git.tradeo.de/<data-project>` bei `besitzer/repo` mit Label `widget` (Direkt-Clients können zusätzlich `type` = `bug`/`feature` setzen) |
| Auth | keine – das Widget ist für interne Apps gedacht, die Identität kommt optional per Hook (siehe unten) |
| Abhängigkeiten | keine. Screenshots nutzen `html2canvas`, das das Widget bei Bedarf selbst von `problem.tradeo.dev` nachlädt |

## Schritt 1: Einbauen (eine Zeile)

Direkt vor `</body>` (oder in das Layout/Template, das auf jeder Seite ist):

```html
<script src="https://problem.tradeo.dev/widget.js" data-project="REPO-NAME" defer></script>
```

- `data-project` **(Pflicht)**: das Gitea-Repo, in dem die Issues landen
  sollen. Für Repos des Standard-Besitzers nur der Repo-Name, z. B.
  `zeiterfassung` für `git.tradeo.de/sebastian/zeiterfassung`. Liegt das Repo
  bei einem anderen Besitzer, `besitzer/repo`, z. B. `Liborius/Ahoch3` – das
  geht nur, wenn der Problemmelder-Betreiber diesen Besitzer freigegeben hat
  (`gitea.extraOwners` im Status-Check), sonst `400` „Besitzer nicht freigegeben“.
- **Freigabe:** Die erste Meldung einer neuen App (Repo + Adresse der Seite)
  wartet auf Freigabe durch einen Problemmelder-Admin, der dazu eine
  Zulip-Nachricht bekommt. Bis dahin antwortet der Server mit `202
  { "ok": true, "pending": true }` und hält bis zu 3 Meldungen zurück; nach der
  Freigabe werden sie als Issues nachgereicht. Nimmt Gitea die Meldung einer
  freigegebenen App nicht an (Repo weg, kein Zugriff, Gitea down), kommt
  `202 { "ok": true, "held": true }`: die Meldung wird zurückgehalten und der
  Admin benachrichtigt.
- **Auto-Freigabe** (wenn im Problemmelder aktiviert): Läuft die App unter
  `https://<name>.tradeo.dev` und wurde ihr Repo schon einmal von Hand
  freigegeben (oder ist es öffentlich), kommt sofort `201` mit Issue – ohne
  Warten auf den Admin. Solche Issues tragen zusätzlich das Label
  `widget:auto`. Mehr als 10 Meldungen pro Stunde und Repo werden
  zurückgehalten (`202 { "ok": true, "held": true }`).
- `data-label` (optional): Anzeigename im Dialog („Bug oder Idee melden: Zeiterfassung“).
  Default ist der Repo-Name.

### Position des Knopfs (optional)

Standardmäßig sitzt der 🐛-Button unten rechts, 16 px vom Rand. Überdeckt er
Inhalte der App (z. B. eine eigene untere Navigationsleiste), lässt er sich
beim Einbinden woanders hinsetzen:

- `data-position`: Ecke, `bottom-right` (Default), `bottom-left`, `top-right`
  oder `top-left`. Unbekannte Werte fallen auf `bottom-right` zurück.
- `data-offset-x`: Abstand zum seitlichen Rand dieser Ecke in px (Default `16`).
- `data-offset-y`: Abstand zum oberen bzw. unteren Rand dieser Ecke in px
  (Default `16`).

Beispiel: links unten, 72 px über dem unteren Rand:

```html
<script src="https://problem.tradeo.dev/widget.js" data-project="REPO-NAME" data-position="bottom-left" data-offset-y="72" defer></script>
```

Next.js:

```tsx
<Script src="https://problem.tradeo.dev/widget.js" data-project="REPO-NAME" data-position="bottom-left" data-offset-y="72" strategy="lazyOnload" />
```

**Vorrang:** Verschiebt ein Nutzer den Knopf (lange drücken, ziehen), merkt
sich sein Browser diese Position pro App (Origin + `data-project`) und nutzt
sie statt `data-position`/`data-offset-*`. Ändern kann der Nutzer sie durch
erneutes Verschieben. Der Knopf bleibt dabei immer im sichtbaren Bereich,
auch wenn das Fenster kleiner wird.

**Hinweis für KI-Assistenten:** Die einzige Rückfrage, die du brauchst, ist
der Repo-Name. Wenn das Projekt selbst in Gitea liegt, ist es fast immer der
Name des eigenen Repos (`git remote -v` zeigt ihn). Gehört das Repo nicht dem
Standard-Besitzer (`gitea.owner` im Status-Check), `besitzer/repo` angeben. Frag nur nach, wenn das
Repo nicht eindeutig ist.

### Framework-Beispiele

- **Plain HTML / Express / PHP:** Script-Tag wie oben ins Basis-Template.
- **Next.js (App Router):** in `app/layout.tsx`
  ```tsx
  import Script from "next/script";
  // …im JSX, innerhalb von <body>:
  <Script src="https://problem.tradeo.dev/widget.js" data-project="REPO-NAME" strategy="lazyOnload" />
  ```
- **Vite / React / Vue:** Script-Tag in `index.html` vor `</body>`.
- **Electron / Desktop-Webviews:** funktioniert, solange der Renderer
  `https://problem.tradeo.dev` erreichen darf.

Nur auf bestimmten Seiten oder nur für eingeloggte Nutzer einbinden? Einfach
das Script-Tag bedingt rendern – das Widget hat keinen weiteren Zustand.

## Schritt 2 (optional): Kontext und Identität mitgeben

Beide Hooks sind optional und können ein Objekt **oder eine Funktion** sein,
die ein Objekt zurückgibt (wird beim Absenden ausgewertet):

```js
// Wer meldet? Landet im Issue als „von **Name**“ + User-Key in der Kontext-Tabelle.
window.problemmelderIdentify = () => ({ userKey: currentUser.email, name: currentUser.name });

// App-Zustand, der beim Debuggen hilft (wird als JSON-Block ins Issue geschrieben, max. 20 kB).
window.problemmelderContext = () => ({ route: location.pathname, tenant: currentTenant.id, version: APP_VERSION });
```

Die alten Käfer-Hooks `window.kaeferIdentify` / `window.kaeferContext` werden
weiterhin gelesen – bestehende Integrationen müssen nur die Script-URL tauschen.

Programmatisch öffnen (z. B. aus einem eigenen Menüpunkt):

```js
window.problemmelder.open();
```

## Was der Nutzer bekommt

1. Klick auf den 🐛-Button (per langem Drücken verschiebbar; die Position wird
   pro App im Browser gemerkt) → Screenshot der
   aktuellen Seite wird aufgenommen, Dialog öffnet sich.
2. Freitext „Was ist passiert oder was wünschst du dir?“ (mit Spracheingabe in Chrome/Edge),
   Screenshot mit **Markierungen** (Rechteck/Freihand), bis zu 10 Anhänge
   (PNG/JPG/GIF/WebP/PDF, je max. 10 MB). Bilder lassen sich auch per ⌘V/Strg+V
   aus der Zwischenablage einfügen (z. B. ein Bildschirmfoto) und werden normale
   Anhänge; das Widget fängt dabei nur Bilddaten ab, Text-Einfügen bleibt wie es ist.
3. **Absenden** → Issue wird angelegt, der Dialog zeigt den Link `Issue #N`.
4. Kein Netz? Die Meldung wird in `localStorage` geparkt (Badge am Button) und
   beim nächsten Seitenaufruf bzw. `online`-Event automatisch nachgesendet.
5. Alternativ **„Mit Zulip-Login senden“**: Öffnet ein Fenster auf
   `problem.tradeo.dev/widget/senden` (Popups müssen erlaubt sein). Dort meldet
   man sich bei Bedarf per Zulip an. Das Fenster zeigt die Meldung
   **vollständig** (Beschreibung, Screenshot, Anhänge, Seite/Browser,
   Console-Log, App-Kontext) und sendet nach „Als <Name> senden“ genau diesen
   Stand. Keine Offline-Queue für diesen Weg.

## Verifizierter Melder (Label `melder:<slug>`)

Nur über „Mit Zulip-Login senden“ prüft der Problemmelder selbst, wer meldet:
über die loginz-Session auf seiner eigenen Origin. Steht dieser Melder in der
Allowlist `WIDGET_MELDER_SLUGS` (z. B. `zulip:8=sebastian`), bekommt das Issue
zusätzlich das Label **`melder:<slug>`**. Der Body beginnt dann mit
„von **Name** (per Zulip-Login bestätigt)“.

- `melder:*`-Labels setzt **nur das Backend**, niemand von Hand.
- `problemmelderIdentify` ist **nie** ein Nachweis. Der normale Senden-Weg
  (`POST /api/v1/widget/reports`) setzt nie ein `melder:*`-Label, egal was im
  Payload steht.
- Bestätigt ist alles, was das Fenster angezeigt hat: Beschreibung,
  Screenshot, Anhänge, Seite/Browser, Console-Log und App-Kontext. Die App
  liefert diese Teile. Wer verifiziert sendet, sollte sie deshalb vor dem
  Klick durchsehen.
- Lässt sich das Label nicht anlegen (Token ohne Label-Recht), entsteht **kein**
  Issue ohne Label. Die Meldung wird zurückgehalten und nachgereicht.
- Voraussetzung auf dem Server: `PUBLIC_BASE_URL` gesetzt (sonst `503`, fail-closed).
  Der Endpunkt `/api/v1/widget/verified-reports` ist nicht cross-origin
  erreichbar und für eigene Clients nicht gedacht.

## So sieht das Issue aus

- **Titel:** erste Zeile der Beschreibung (mit `type` aus der API: Präfix `[Bug]` / `[Feature]`)
- **Labels:** `widget` (plus `bug`/`feature`, wenn `type` gesetzt; plus `melder:<slug>` bei verifiziertem Melder, siehe oben); fehlende Labels werden im Repo angelegt
- **Body:** Beschreibung, Kontext-Tabelle (Seite, Viewport + Farbschema,
  Browser, User), Screenshot als Bild, Anhänge als Links, Console-Log
  (Zusammenfassung + letzte 40 Zeilen aufklappbar, komplett als Anhang
  `console.log`), App-Kontext als JSON-Block.

**Console-Log:** Das Widget zeichnet ab seinem Laden `console.log/info/warn/
error/debug`, unbehandelte Fehler und Promise-Rejections auf (Ringpuffer, die
letzten 200 Einträge, je max. 2000 Zeichen). Ausgaben **vor** dem Laden des
Widgets fehlen – wer Start-Fehler sehen will, bindet das Script möglichst früh
ein (z. B. im `<head>` mit `defer`).

Screenshot und Anhänge werden als **Issue-Assets** in Gitea hochgeladen; der
Server prüft den Dateityp über Magic Bytes (kein SVG/HTML/Script).

## Sicherheit & Grenzen

- Das Widget sendet **cross-origin** per `fetch` an `problem.tradeo.dev`; CORS
  ist nur für die Widget-Endpunkte für beliebige Origins freigegeben, ohne
  Cookies. Die App und das Widget kennen keine Gitea-Zugangsdaten – der Token
  liegt nur auf dem Problemmelder-Server.
- Jede App muss einmal im Problemmelder-Admin („Widget-Apps“) freigegeben
  werden (siehe oben); gesperrte Apps bekommen `403`.
- Hat die App eine `Cross-Origin-Opener-Policy` (`same-origin`), verliert das
  Bestätigungsfenster die Verbindung zur App. Dann geht nur der normale Senden-Weg.
- **Content-Security-Policy:** Falls die App eine CSP hat, müssen
  `script-src` und `connect-src` `https://problem.tradeo.dev` erlauben.
  Fehlt `script-src`, kann `html2canvas` nicht geladen werden – der Dialog
  bietet dann „Bildschirm aufnehmen“ / „Bild hochladen“ als Ersatz an.
- Rate-Limit: 30 Meldungen pro Minute und IP. Body-Limit 32 MB.
- Es gibt **kein** Dashboard im Problemmelder für diese Meldungen – die
  Bearbeitung passiert komplett in den Gitea-Issues des jeweiligen Repos.

## API direkt nutzen (ohne Widget)

Eigene Clients (CLI, Backend, Mobile) können denselben Endpoint aufrufen:

```http
POST https://problem.tradeo.dev/api/v1/widget/reports
Content-Type: application/json

{
  "project": "zeiterfassung",          // oder "besitzer/repo" (freigegebener Besitzer)
  "type": "bug",                       // optional: "bug" | "feature" (Widget sendet keinen Typ)
  "description": "Stempeln um 06:00 wirft Fehler",
  "url": "https://zeit.tradeo.dev/stempeln",
  "userAgent": "…", "viewport": "390x844", "colorScheme": "dark",
  "consoleLog": [{ "t": 1789000000000, "level": "error", "msg": "TypeError: …" }],
  "customData": { "route": "/stempeln" },
  "identify": { "userKey": "lena@example.com", "name": "Lena" },
  "screenshot": "data:image/jpeg;base64,…",
  "attachments": [{ "data": "data:application/pdf;base64,…", "name": "beleg.pdf" }]
}
```

Antwort `201`:

```json
{ "ok": true, "issue": { "repo": "sebastian/zeiterfassung", "number": 42, "url": "https://git.tradeo.de/sebastian/zeiterfassung/issues/42" }, "attachments": { "saved": 2, "skipped": 0 } }
```

Solange die App nicht freigegeben ist: `202 { "ok": true, "pending": true }`
(mit aktiver Auto-Freigabe gilt eine App unter `https://<name>.tradeo.dev`
sofort als freigegeben, wenn ihr Repo schon einmal von Hand freigegeben wurde
oder öffentlich ist).
Hat Gitea die Meldung nicht angenommen oder ist das Stundenlimit automatisch
freigegebener Apps erreicht: `202 { "ok": true, "held": true }` (wird nachgereicht).

Fehler: `400` ungültige Eingabe (Details im Body), `403` App gesperrt,
`429` Rate-Limit, `502` Gitea nimmt nichts an und der Zwischenspeicher ist voll, `503` Widget serverseitig
nicht konfiguriert.

## Checkliste

1. Repo-Name klären (`data-project`; bei fremdem Besitzer `besitzer/repo`).
2. Script-Tag einbauen, optional `problemmelderIdentify` / `problemmelderContext` setzen.
3. CSP prüfen (`script-src`, `connect-src`).
4. Testen: 🐛 klicken → Bug beschreiben → Absenden → Link `Issue #N` erscheint
   und das Issue liegt im Repo mit Label `widget`.
