docs: add killer-features planning document (blind meeting points, verifiable listings, self-destructing listings)
This commit is contained in:
438
docs/KILLER-FEATURES.md
Normal file
438
docs/KILLER-FEATURES.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user