diff --git a/docs/KILLER-FEATURES.md b/docs/KILLER-FEATURES.md new file mode 100644 index 0000000..37c5f13 --- /dev/null +++ b/docs/KILLER-FEATURES.md @@ -0,0 +1,438 @@ +# Killer-Features — dgray.io + +Differenzierung gegenüber eBay Kleinanzeigen, Tutti, XMRBazaar. +Drei Features, die kein Konkurrent hat. + +Status: **Planung** (noch nicht implementiert) + +--- + +## 1. Blind Meeting Points + +**Problem:** Bei lokalen Deals muss eine Seite ihre Adresse preisgeben. Privacy-bewusste User vermeiden das — oder treffen sich an zufälligen, unsicheren Orten. + +**Lösung:** Beide Parteien geben einen ungefähren Radius an (z.B. "Zürich HB ± 1 km"). Die App berechnet einen **öffentlichen, neutralen Treffpunkt** im Überschneidungsbereich (Bahnhof, Einkaufszentrum, Café). Keine Seite gibt ihre echte Adresse preis. + +### Datenquelle + +- **OpenStreetMap Overpass API** (kostenlos, kein API-Key) +- Query: POIs mit Tags wie `amenity=cafe`, `shop=mall`, `railway=station` im Überschneidungskreis +- Fallback: Mittelpunkt der beiden Radien als generischer Treffpunkt + +### Algorithmus + +``` +1. Seller setzt Punkt A + Radius rA (z.B. 1 km) +2. Buyer setzt Punkt B + Radius rB (z.B. 1.5 km) +3. Berechne Überschneidungsfläche der beiden Kreise +4. Falls keine Überschneidung: Mittelpunkt A↔B vorschlagen, Radien vergrößern +5. Query Overpass API für geeignete POIs in der Überschnittfläche +6. Filtere nach Kategorien (priorisiert): + a. Bahnhöfe / ÖV-Haltestellen (öffentlich, belebt) + b. Einkaufszentren (überdacht, sicher) + c. Cafés / Restaurants (neutral) + d. Parks / Plätze (Fallback) +7. Wähle den POI, der am nächsten zum Mittelpunkt liegt +8. Zeige Kartenausschnitt (Leaflet + OSM-Tiles) mit dem Treffpunkt +``` + +### UX-Flow + +``` +Chat-Widget: +┌─────────────────────────────────────┐ +│ 💬 Chat mit Käufer │ +│ │ +│ ... Nachrichten ... │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ 📍 Treffpunkt vorschlagen │ │ +│ └─────────────────────────────┘ │ +│ │ +│ → Klick öffnet Modal: │ +│ ┌─────────────────────────────┐ │ +│ │ 🗺 Karte (Leaflet/OSM) │ │ +│ │ │ │ +│ │ [Pin auf Karte setzen] │ │ +│ │ Radius: [====●===] 1 km │ │ +│ │ │ │ +│ │ [Treffpunkt berechnen] │ │ +│ └─────────────────────────────┘ │ +│ │ +│ Ergebnis wird als Chat-Nachricht │ +│ gesendet (E2E verschlüsselt): │ +│ ┌─────────────────────────────┐ │ +│ │ 📍 Vorschlag: Zürich HB │ │ +│ │ Bahnhofstrasse 1 │ │ +│ │ [Auf Karte anzeigen] │ │ +│ └─────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +### Technische Umsetzung + +| Komponente | Datei | Beschreibung | +|------------|-------|--------------| +| Service | `js/services/meeting-points.js` | Geo-Berechnung, Overpass-API-Query, POI-Filterung | +| Component | `js/components/meeting-point-modal.js` | Karte (Leaflet), Radius-Slider, Ergebnis-Anzeige | +| Integration | `js/components/chat-widget.js` | Button "Treffpunkt vorschlagen", Ergebnis als Nachricht | +| CSS | `css/components.css` | Karten-Styles, Modal-Styles | +| Vendor | `js/vendor/leaflet/` | Leaflet.js (self-hosted, ~40 KB gzip) | + +### Overpass API Query (Beispiel) + +``` +[out:json][timeout:10]; +( + node["railway"="station"](around:500,47.3769,8.5417); + node["amenity"="cafe"](around:500,47.3769,8.5417); + node["shop"="mall"](around:500,47.3769,8.5417); +); +out center 5; +``` + +### Privacy-Aspekte + +- **Kein serverseitiger Standort**: Berechnung komplett im Browser (Client-Side) +- **Overpass-Query**: Geht direkt an OSM-Server (kein Proxy nötig, keine Logs bei uns) +- **Treffpunkt im Chat**: E2E verschlüsselt, Server sieht nur Ciphertext +- **Kein GPS nötig**: User setzt Pin manuell auf der Karte +- Seller-Standort ≠ Treffpunkt (Treffpunkt ist immer ein öffentlicher Ort) + +### i18n Keys + +``` +meeting.suggest = "Treffpunkt vorschlagen" +meeting.setLocation = "Deinen ungefähren Standort wählen" +meeting.radius = "Radius" +meeting.calculate = "Treffpunkt berechnen" +meeting.result = "Vorgeschlagener Treffpunkt" +meeting.noOverlap = "Kein gemeinsamer Bereich — Mittelpunkt wird vorgeschlagen" +meeting.noPoi = "Kein öffentlicher Ort gefunden — Punkt auf der Karte" +meeting.send = "Im Chat senden" +meeting.showMap = "Auf Karte anzeigen" +meeting.types.station = "Bahnhof" +meeting.types.mall = "Einkaufszentrum" +meeting.types.cafe = "Café" +meeting.types.park = "Park / Platz" +``` + +### Aufwand + +- **Geschätzt**: 2–3 Tage +- **Dependencies**: Leaflet.js (self-hosted), Overpass API (kostenlos) +- **Risiko**: Overpass API Rate-Limits (Fallback: Mittelpunkt ohne POI-Suche) + +--- + +## 2. Verifiable Listings (Proof of Possession) + +**Problem:** Auf anonymen Marktplätzen ist Scam ein Hauptproblem. Seller können Bilder aus dem Internet nehmen und Artikel listen, die sie nicht besitzen. Ohne KYC gibt es keine Konsequenzen. + +**Lösung:** Seller können optional ein Foto hochladen, auf dem ein von der App generierter **Einmal-Verifizierungscode** sichtbar ist (auf Zettel, Display, etc.). Das beweist: "Dieser Artikel war zum Zeitpunkt X physisch vorhanden." + +### Verfahren + +``` +1. Seller klickt "Besitz verifizieren" beim Listing erstellen/bearbeiten +2. App generiert einen 6-stelligen Code + QR-Code + Zeitstempel + - Code: kryptografisch zufällig (crypto.getRandomValues) + - Gültig: 10 Minuten (danach neuer Code) +3. Seller fotografiert den Artikel MIT dem Code (auf Zettel, Handy-Display daneben, etc.) +4. Foto wird hochgeladen als spezielles Verifikations-Bild +5. App speichert: Code + Zeitstempel + Foto-ID als Verifikationsnachweis +6. Listing zeigt Badge: "✓ Besitz verifiziert am [Datum]" +``` + +### Verifikations-Sicherheit + +| Aspekt | Lösung | +|--------|--------| +| Fake-Code auf echtem Bild | Code ist nur 10 Min gültig, Zeitfenster wird angezeigt | +| Photoshop / Bildbearbeitung | EXIF-Metadaten prüfen (optional), Hash des Originalbilds speichern | +| Fremdes Foto mit Code | Nicht 100% verhinderbar, aber Aufwand steigt deutlich | +| Wiederverwendung | Code ist einmalig, an Listing-ID gebunden | + +**Wichtig:** Das ist kein perfekter Beweis, sondern ein **Vertrauenssignal**. Es erhöht den Aufwand für Scammer signifikant. + +### Datenmodell (Directus) + +Neues Feld auf `listings`: + +| Feld | Typ | Beschreibung | +|------|-----|--------------| +| `verification_code` | String (6) | Der generierte Code | +| `verification_image` | UUID, FK → directus_files | Das Verifikationsfoto | +| `verification_date` | DateTime | Zeitpunkt der Verifikation | +| `verified` | Boolean | `true` wenn Verifikation abgeschlossen | + +### UX-Flow + +``` +Listing erstellen / bearbeiten: +┌─────────────────────────────────────┐ +│ 📷 Bilder hochladen │ +│ [Bild 1] [Bild 2] [+] │ +│ │ +│ ───────────────────────────── │ +│ │ +│ ✓ Besitz verifizieren (optional) │ +│ │ +│ → Klick öffnet Verifikation: │ +│ ┌─────────────────────────────┐ │ +│ │ │ │ +│ │ Dein Code: 4 8 2 7 1 5 │ │ +│ │ Gültig noch: 08:42 │ │ +│ │ │ │ +│ │ Schreibe diesen Code auf │ │ +│ │ einen Zettel und fotografiere│ │ +│ │ deinen Artikel zusammen │ │ +│ │ mit dem Code. │ │ +│ │ │ │ +│ │ [📷 Verifikationsfoto │ │ +│ │ hochladen] │ │ +│ │ │ │ +│ └─────────────────────────────┘ │ +│ │ +│ Nach Upload: │ +│ ✅ Besitz verifiziert │ +└─────────────────────────────────────┘ + +Auf dem Listing (für Käufer): +┌──────────────────────┐ +│ 📷 Bild │ +│ Titel der Anzeige │ +│ 50.00 EUR │ +│ ✓ Besitz verifiziert│ ← Badge +│ 📍 Zürich 🟢 10+ │ +└──────────────────────┘ +``` + +### Technische Umsetzung + +| Komponente | Datei | Beschreibung | +|------------|-------|--------------| +| Service | `js/services/verification.js` | Code-Generierung, Validierung, Directus CRUD | +| Component | `js/components/verification-widget.js` | Code-Anzeige, Countdown, Upload | +| Integration | `js/components/pages/page-create.js` | Einbettung in Listing-Formular | +| Integration | `js/components/pages/page-listing.js` | Badge-Anzeige für Käufer | +| CSS | `css/components.css` | Badge-Styles, Verifikations-Widget | + +### Privacy-Aspekte + +- **Freiwillig**: Keine Pflicht, reines Opt-in +- **Kein KYC**: Code beweist Besitz, nicht Identität +- **Foto-Metadaten**: EXIF wird beim Upload gestrippt (wie bei normalen Listing-Bildern) +- **Code serverseitig**: Nur der Hash des Codes wird gespeichert (nicht der Klartext) + +### i18n Keys + +``` +verification.verify = "Besitz verifizieren" +verification.optional = "Optional — erhöht das Vertrauen" +verification.code = "Dein Code" +verification.validFor = "Gültig noch" +verification.instructions = "Schreibe diesen Code auf einen Zettel und fotografiere deinen Artikel zusammen mit dem Code." +verification.upload = "Verifikationsfoto hochladen" +verification.verified = "Besitz verifiziert" +verification.verifiedDate = "Verifiziert am {{date}}" +verification.expired = "Code abgelaufen — neuen generieren" +verification.badge = "✓ Verifiziert" +``` + +### Aufwand + +- **Geschätzt**: 1–2 Tage +- **Dependencies**: Keine neuen (Directus File-Upload existiert bereits) +- **Risiko**: Gering — Feature ist optional und unabhängig + +--- + +## 3. Selbstzerstörende Listings + +**Problem:** Auf klassischen Plattformen bleiben Anzeigen und Nutzerspuren ewig gespeichert. Archivierte Listings sind oft noch über Suchmaschinen auffindbar. Für Privacy-bewusste User ist das ein Dealbreaker. + +**Lösung:** Seller können ein Listing als **selbstzerstörend** markieren. Nach Ablauf oder Deal-Abschluss werden alle Daten (Bilder, Beschreibung, Verifikation) **unwiderruflich gelöscht** — nicht archiviert. + +### Modi + +| Modus | Trigger | Effekt | +|-------|---------|--------| +| **Zeitbasiert** | Seller wählt Lebensdauer: 24h, 48h, 7 Tage, 30 Tage | Listing + Bilder werden nach Ablauf gelöscht | +| **Deal-basiert** | Deal wird beidseitig bestätigt | Listing wird X Stunden nach Deal-Bestätigung gelöscht | +| **Manuell** | Seller klickt "Jetzt zerstören" | Sofortige Löschung | + +**Standard-Listings** (ohne Selbstzerstörung) verhalten sich wie bisher: Archivierung nach 30 Tagen. + +### Löschung — was wird gelöscht? + +``` +1. Listing-Datensatz (Directus: items.delete) +2. Alle verknüpften Bilder (Directus Files: files.delete) +3. Verifikationsfoto (falls vorhanden) +4. Junction-Table-Einträge (listings_files) +5. Conversations + Messages werden NICHT gelöscht + (die sind E2E verschlüsselt und gehören beiden Parteien) +``` + +### Datenmodell (Directus) + +Neue Felder auf `listings`: + +| Feld | Typ | Beschreibung | +|------|-----|--------------| +| `self_destruct` | Boolean | `true` wenn Selbstzerstörung aktiv | +| `destruct_mode` | String | `time`, `deal`, `manual` | +| `destruct_at` | DateTime | Zeitpunkt der geplanten Löschung | +| `destruct_after_deal_hours` | Integer | Stunden nach Deal-Bestätigung (Default: 24) | + +### Implementierung: Löschung + +**Option A: Directus Flow (empfohlen)** +- Neuer Scheduled Flow: alle 15 Minuten (wie Expired-Listings-Flow) +- Query: `self_destruct = true AND destruct_at <= NOW()` +- Action: Delete Listing + zugehörige Files +- Vorteil: Serverseitig, zuverlässig + +**Option B: Frontend-Trigger (Fallback)** +- Bei App-Start prüfen: Gibt es eigene Listings mit `destruct_at` in der Vergangenheit? +- Falls ja: Delete-Request an Directus +- Nachteil: Funktioniert nur wenn Seller die App öffnet + +**Empfehlung:** Option A als Primär, Option B als zusätzliche Absicherung. + +### UX-Flow + +``` +Listing erstellen: +┌─────────────────────────────────────┐ +│ Titel: [___________________] │ +│ Preis: [___] Währung: [EUR ▼] │ +│ ... │ +│ │ +│ ───────────────────────────── │ +│ 🔥 Selbstzerstörung │ +│ │ +│ [●] Aus (Standard: Archivierung) │ +│ [ ] Nach Zeitablauf │ +│ → [24h ▼] / 48h / 7d / 30d │ +│ [ ] Nach Deal-Abschluss │ +│ → [24] Stunden danach │ +│ │ +│ ⚠ Achtung: Gelöschte Anzeigen │ +│ können nicht wiederhergestellt │ +│ werden. │ +└─────────────────────────────────────┘ + +Auf dem Listing (für Käufer): +┌──────────────────────┐ +│ 📷 Bild │ +│ Titel der Anzeige │ +│ 50.00 EUR │ +│ 🔥 Läuft ab in 23h │ ← Countdown +│ 📍 Zürich │ +└──────────────────────┘ + +Für Seller (eigenes Listing): +┌──────────────────────────────┐ +│ 🔥 Selbstzerstörung aktiv │ +│ Wird gelöscht am: 12.02.26 │ +│ [Jetzt zerstören] │ +└──────────────────────────────┘ +``` + +### Technische Umsetzung + +| Komponente | Datei | Beschreibung | +|------------|-------|--------------| +| Service | `js/services/self-destruct.js` | Countdown-Berechnung, Delete-Requests, Deal-Trigger | +| Directus Flow | (Server-Config) | Scheduled Flow: Listings mit `destruct_at <= NOW()` löschen | +| Integration | `js/components/pages/page-create.js` | Selbstzerstörungs-Optionen im Formular | +| Integration | `js/components/pages/page-listing.js` | Countdown-Anzeige, "Jetzt zerstören"-Button | +| Integration | `js/components/listing-card.js` | Countdown-Badge auf Karten | +| CSS | `css/components.css` | Countdown-Badge, Warnungs-Styles | + +### Directus Flow: Auto-Delete + +``` +Trigger: Schedule (*/15 * * * *) +Bedingung: listings WHERE self_destruct = true AND destruct_at <= NOW() + +Aktionen: +1. Read: Betroffene Listings + deren File-IDs laden +2. Delete: Files löschen (directus_files) +3. Delete: Junction-Einträge löschen (listings_files) +4. Delete: Listings löschen +``` + +### Deal-basierte Zerstörung + +``` +Wenn Deal status → confirmed: +1. Prüfe: Hat das Listing self_destruct = true UND destruct_mode = 'deal'? +2. Ja → Setze destruct_at = NOW() + destruct_after_deal_hours +3. Ab dann greift der Scheduled Flow +``` + +Integration in `js/services/reputation.js` → `confirmDeal()`: + +```js +// Nach beidseitiger Bestätigung: +if (listing.self_destruct && listing.destruct_mode === 'deal') { + const destructAt = new Date() + destructAt.setHours(destructAt.getHours() + (listing.destruct_after_deal_hours || 24)) + await directus.updateItem('listings', listing.id, { destruct_at: destructAt.toISOString() }) +} +``` + +### Privacy-Aspekte + +- **Echte Löschung**: Kein Soft-Delete, kein Archiv — Daten werden unwiderruflich entfernt +- **Bilder inklusive**: Files werden aus Directus Storage gelöscht (nicht nur DB-Einträge) +- **Keine Suchmaschinen-Spuren**: `noindex` Meta-Tag für Listings mit Selbstzerstörung +- **Countdown sichtbar**: Käufer sehen, dass das Listing zeitlich begrenzt ist (schafft Urgency) + +### i18n Keys + +``` +destruct.title = "Selbstzerstörung" +destruct.off = "Aus (Standard)" +destruct.time = "Nach Zeitablauf" +destruct.deal = "Nach Deal-Abschluss" +destruct.manual = "Jetzt zerstören" +destruct.duration.24h = "24 Stunden" +destruct.duration.48h = "48 Stunden" +destruct.duration.7d = "7 Tage" +destruct.duration.30d = "30 Tage" +destruct.afterDeal = "{{hours}} Stunden nach Deal" +destruct.warning = "Gelöschte Anzeigen können nicht wiederhergestellt werden." +destruct.active = "Selbstzerstörung aktiv" +destruct.expiresIn = "Läuft ab in {{time}}" +destruct.deletedAt = "Wird gelöscht am {{date}}" +destruct.confirm = "Anzeige unwiderruflich löschen?" +destruct.destroyed = "Diese Anzeige wurde gelöscht" +``` + +### Aufwand + +- **Geschätzt**: 1.5–2 Tage +- **Dependencies**: Directus Flow (analog zu bestehender Expired-Listings-Flow) +- **Risiko**: Directus File-Löschung muss getestet werden (Cascade) + +--- + +## Zusammenfassung + +| # | Feature | Aufwand | Einzigartigkeit | Privacy-Impact | +|---|---------|---------|-----------------|----------------| +| 1 | Blind Meeting Points | 2–3 Tage | Sehr hoch — kein Konkurrent hat das | Hoch | +| 2 | Verifiable Listings | 1–2 Tage | Hoch — innovative Lösung ohne KYC | Mittel | +| 3 | Selbstzerstörende Listings | 1.5–2 Tage | Hoch — starkes Privacy-Signal | Sehr hoch | + +**Gesamt: ~5–7 Tage Implementierung** + +### Empfohlene Reihenfolge + +1. **Verifiable Listings** — geringster Aufwand, sofort sichtbarer Wert, keine neue Dependency +2. **Selbstzerstörende Listings** — baut auf bestehendem Flow auf, starkes Marketingmerkmal +3. **Blind Meeting Points** — höchster Aufwand, aber stärkstes Alleinstellungsmerkmal