seepuls/docs/SCREENSHOT_SUBMIT.md
Till JS 0465d16d6b fix(submit): mana-llm Default-Modell google/gemini-2.5-flash statt gpt-4o-mini
Empirisch gegen llm.mana.how (2026-05-27): mana-llm bedient ollama/* +
google/*, KEIN gpt-4o-mini (→ 500 no_healthy_provider). google/gemini-2.5-
flash ist verfügbar + vision-fähig. Default jetzt env-überschreibbar
(MANA_LLM_MODEL / MANA_LLM_VISION_MODEL).

Korrigiert zugleich die F1-Annahme in docs/SCREENSHOT_SUBMIT.md: der
mana-auth-Service-Key war NIE der Vision-Blocker — llm.mana.how hat Auth
aus (GPU_API_KEY leer). Key bleibt nur für den Crawl-Pfad relevant
(research.mana.how → 401).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:21:23 +02:00

175 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Screenshot-Submit — „Tipp einreichen"
Stand: **2026-05-26**. Phase **σ-submit-0 (Foundation)** in Arbeit.
## Problem
Clubs, Bars und selbstverwaltete Häuser (Contrast, P-Club, Kula …)
publizieren ihr Programm fast nur auf **Instagram/Facebook-Stories**
genau dort, wo der Aggregator-Crawler **nicht** hinkommt:
- `AGGREGATOR_POLICY.md §1` verbietet Login-Bypass („Inhalte hinter Login
werden nicht gecrawlt, Punkt") **nicht-verhandelbar**.
- Meta-ToS verbietet automatisierten Zugriff mit eingeloggtem Account;
Bann + Rechts-Exposure (DSGVO, §87b UrhG) wären die Folge.
Resultat: Von 63 curated Venues haben nur 7 Events. Alle Clubs/Bars —
inkl. Contrast mit fester Dienstagsparty — sind leer. Der Crawl-Weg ist
für diese Kategorie strukturell verschlossen.
## Lösung
**Der Mensch macht den Zugriff, die Maschine die Struktur.** Eine
Nutzer:in sieht einen Post in *ihrem* Feed, screenshottet ihn und teilt
ihn an Seepuls. mana-llm-Vision extrahiert die **Tatsachen** (Titel,
Datum, Ort), ein Mensch moderiert, dann wird der Event publiziert.
Das ist die Memoro-Logik (Input legitim vom Menschen, KI verarbeitet) —
und kategorisch anders als ein Scraper: **Seepuls berührt Instagram nie.**
```
Nutzer:in sieht Post/Story ──screenshot──► Share-Sheet / Upload / Mail
POST /api/v1/submissions
{ image, post_url?, note? }
mana-llm Vision → { title, starts_at, venue_hint, … }
│ (best-effort; Bild = Input)
Venue-Matching (Fuzzy → 63 Venues)
event_submissions (status: pending)
│ Bild nach Entscheidung gepurged
Moderations-Queue (Admin)
│ bestätigen / korrigieren
events (live, „gemeldet · Quelle …")
```
## Die drei nicht-verhandelbaren Regeln
Aus `AGGREGATOR_POLICY.md` abgeleitet. Test-pflichtig.
1. **Screenshot ist Input, nie Output.** Wir extrahieren *Tatsachen*
(§5: „Tatsachen sind frei"). Das **Bild, das Flyer-Artwork, der
1:1-Text werden nie re-publiziert.** Der öffentliche Event zeigt
Titel + Datum + Ort + ≤200 Zeichen + Quell-Link. Der rohe Screenshot
wird nach der Moderations-Entscheidung **gepurged** (`imagePurgedAt`).
2. **Es ist ein Tipp, kein Claim.** Wer screenshottet, ist meist nicht
der Betreiber. Also: **Moderations-Queue statt Direkt-Publish**,
Attribution „von Nutzer:in gemeldet · Quelle: instagram.com/…",
**nie** „verifiziert vom Betreiber". Take-Down-Flow (§4b) bleibt voll.
3. **Quelle mit-erfassen.** Submit fragt zusätzlich nach dem Post-Link
(für §2-Attribution). Fehlt er, liest die Vision den @-Handle und wir
verlinken aufs Profil.
DSGVO-Fußnote: Screenshots können Gesichter/Namen enthalten → schmale,
zweckgebundene Verarbeitung (Fakten ziehen), Bild danach löschen,
Take-Down bleibt. Kein Bild-Archiv.
## Foundation-Gaps (was auf der Plattform noch nicht läuft)
| # | Gap | Ebene | Status |
|---|---|---|---|
| F1 | ~~Service-Key für seepuls bei mana-auth~~**falsche Annahme.** Empirisch (2026-05-27): `llm.mana.how` hat **Auth aus** (`GPU_API_KEY` leer) → Vision braucht **keinen** Key. Der echte Blocker war das **Modell**: seepuls-Default `gpt-4o-mini` → 500 (mana-llm bedient `ollama/*`+`google/*`). Fix: Default auf `google/gemini-2.5-flash` (vision-fähig, env-überschreibbar). **Hinweis:** `research.mana.how` verlangt sehr wohl einen Key (401) — relevant nur für den **Crawl**-Pfad, nicht für Submit. | Code | ✅ Modell-Fix; Key nur für Crawl offen |
| F2 | seepuls mana-llm-Client kann **nur Text**, kein Bild | Code | dieser Build |
| F3 | `event_submissions`-Tabelle existiert nicht | Code | dieser Build |
| F4 | `share_receive`-Handler leer, `accepts: []` | Code | dieser Build (eigener `/submissions`-Endpoint) |
| F5 | Vision-Extraction-Service fehlt | Code | dieser Build |
| F6 | Venue-Matching fehlt | Code | dieser Build |
| F7 | Moderations-Route fehlt | Code | dieser Build |
| F8 | Transienter Bild-Speicher + Purge-Cron (mana-media) | Code/Ops | Phase 2 |
| F9 | Submit-UI (Web + PWA share_target + native Share-Extension) | Code | Phase 3 |
**Graceful Degradation als Design-Prinzip:** Die Vision (F1/F2/F5) ist
eine *Anreicherung*, kein Hard-Block. Fehlt der Service-Key, landet die
Submission trotzdem in der Queue — mit Notiz + Bild-Referenz, und die
Moderator:in füllt die Felder manuell. So funktioniert Submit + Moderation
**ohne** den Plattform-Onboarding-Schritt; die KI macht's nur bequemer.
## Phasen
### σ-submit-0 — Foundation (dieser Build, Branch `feat/screenshot-submit`)
- `event_submissions`-Schema + Migration (F3)
- mana-llm `extractStructuredFromImage()` (F2)
- `services/screenshot-extraction.ts` — Vision→Fakten, graceful (F5)
- `services/venue-match.ts` — reine Fuzzy-Funktion, unit-getestet (F6)
- `routes/public/submissions.ts` — POST-Intake, public, IP-Hash (F4)
- `routes/admin/submissions.ts` — list / approve→event / reject (F7)
- Mount in `index.ts`, Manifest `accepts`-Eintrag
- Unit-Tests + type-check grün
### σ-submit-1 — Live schalten (braucht Deploy)
- ✅ Modell-Fix (`google/gemini-2.5-flash` statt `gpt-4o-mini`) — kein
Service-Key nötig (mana-llm Auth aus). **Deploy steht noch aus:** Submit-
Code lebt auf `feat/screenshot-submit`, prod fährt alten Code.
- Live-Vision-Smoke gegen `llm.mana.how` (nach Deploy) — incl. `max_tokens`
hoch genug für Geminis Thinking + `response_format`-Verhalten verifizieren.
- F8: transienter Bild-Speicher via mana-media + Purge-Cron (Bilder >7d
pending → löschen)
- Rate-Limit (Token-Bucket pro IP-Hash, analog Take-Down-TODO)
### σ-submit-2 — UI (F9)
- ✅ Web `/melden`-Seite (Upload + Post-Link + Notiz), server-seitiger POST
- ✅ PWA Web-Share-Target (`manifest.webmanifest` share_target → `/melden`)
- ✅ Moderations-UI (Admin) — `/admin/moderation`, Queue + Approve→Event /
Reject. Client-seitig mit mana-auth-Bearer-Token (Admin-Routen sind
JWT-gegated), noindex + robots-Disallow. Damit ist der Loop
Submit→Moderation→Publish zu.
- ⏳ seepuls-native Share-Extension (σ-Native) → POST `/submissions`
- ⏳ Service-Worker für zuverlässige PWA-Installation (Share-POST klappt
nach Installation auch ohne)
## Datenmodell
`event_submissions` — eine eingereichte Meldung, vor Moderation.
```
id sub_…
status pending | approved | rejected | spam
channel screenshot | share-target | federation | manual
note optionale Nutzer-Notiz
postUrl optionaler Link auf den Original-Post (§2-Attribution)
imageRef transiente Bild-Referenz (mana-media-Key o. lokal)
imagePurgedAt gesetzt, sobald Bild gelöscht
extracted jsonb { title, starts_at, ends_at, venue_hint, … }
extractionStatus pending | ok | failed | skipped
extractionError Fehlertext, falls Vision scheiterte
matchedVenueId → venues.id (Fuzzy-Treffer, von Mod bestätigt)
venueHint roher Name/@-Handle aus dem Post
resultEventId → events.id, gesetzt bei approve
reviewedByUserId Moderator:in
reviewedAt Zeitpunkt der Entscheidung
moderatorNote freier Vermerk
submitterUserId optional, wenn eingeloggt
clientIpHash Anti-Spam (sha256(IP), kein Roh-IP) — wie Take-Down
createdAt / updatedAt
```
Approve → erzeugt eine `events`-Row (Dedupe via `external_id_hash`,
`source_url` = postUrl, `event_source_id` = null = „gemeldet").
## API
### Public (kein Auth, IP-Hash + Rate-Limit)
- `POST /api/v1/submissions``{ image (base64/multipart), post_url?, note?, venue_hint? }`
`{ id, status: 'pending' }`. Best-effort-Vision synchron oder als Job.
### Admin (JWT, mana-auth)
- `GET /api/v1/admin/submissions?status=pending`
- `POST /api/v1/admin/submissions/:id/approve``{ venueId, title, starts_at, … (Korrekturen) }` → erzeugt Event
- `POST /api/v1/admin/submissions/:id/reject``{ reason }`
## Offene Punkte
- **F1 ist der eine echte Plattform-Blocker** für die KI-Stufe — eine
einmalige Ops-Aktion (Service-Key bei mana-auth). Bis dahin läuft die
Pipeline im Manuell-Modus (graceful degrade).
- **Spam:** Start anonym + Moderation + IP-Hash-Rate-Limit; Login (mana-auth)
optional fürs „meine Meldungen"-Tracking. Local-First/Login-Optional-Linie.
- **Bild-Speicher:** MVP hält `imageRef`; ob mana-media-Bucket oder kurzer
lokaler Pfad entscheidet σ-submit-1. Nie öffentlich servieren.
- **Manifest-`accepts`:** vorerst seepuls-lokaler `/submissions`-Endpoint.
Ob „Screenshot→Event" ein geteilter Typ im `@mana/shared-share-protocol`
wird (andere Apps könnten ihn anbieten), ist eine mana-Repo-Architektur-
Frage → vor σ-submit-2 mit `mana-architect` klären.
- **Venue-Matching-Schwelle:** Fuzzy-Match liefert Kandidaten + Score; die
finale Zuordnung trifft die Moderator:in, nie der Automat.