refactor(docs): réorganiser la documentation selon principes DDD

Réorganise la documentation du projet selon les principes du Domain-Driven Design (DDD) pour améliorer la cohésion, la maintenabilité et l'alignement avec l'architecture modulaire du backend.

**Structure cible:**
```
docs/domains/
├── README.md (Context Map)
├── _shared/ (Core Domain)
├── recommendation/ (Supporting Subdomain)
├── content/ (Supporting Subdomain)
├── moderation/ (Supporting Subdomain)
├── advertising/ (Generic Subdomain)
├── premium/ (Generic Subdomain)
└── monetization/ (Generic Subdomain)
```

**Changements effectués:**

Phase 1: Création de l'arborescence des 7 bounded contexts
Phase 2: Déplacement des règles métier (01-19) vers domains/*/rules/
Phase 3: Déplacement des diagrammes d'entités vers domains/*/entities/
Phase 4: Déplacement des diagrammes flux/états/séquences vers domains/*/
Phase 5: Création des README.md pour chaque domaine
Phase 6: Déplacement des features Gherkin vers domains/*/features/
Phase 7: Création du Context Map (domains/README.md)
Phase 8: Mise à jour de mkdocs.yml pour la nouvelle navigation
Phase 9: Correction automatique des liens internes (script fix-markdown-links.sh)
Phase 10: Nettoyage de l'ancienne structure (regles-metier/, diagrammes/, features/)

**Configuration des tests:**
- Makefile: godog run docs/domains/*/features/
- scripts/generate-bdd-docs.py: features_dir → docs/domains

**Avantages:**
 Cohésion forte: toute la doc d'un domaine au même endroit
 Couplage faible: domaines indépendants, dépendances explicites
 Navigabilité améliorée: README par domaine = entrée claire
 Alignement code/docs: miroir de backend/internal/
 Onboarding facilité: exploration domaine par domaine
 Tests BDD intégrés: features au plus près des règles métier

Voir docs/REFACTOR-DDD.md pour le plan complet.
This commit is contained in:
jpgiannetti
2026-02-07 17:15:02 +01:00
parent 78422bb2c0
commit 5e5fcf4714
227 changed files with 1413 additions and 1967 deletions

View File

@@ -0,0 +1,420 @@
## 2. Algorithme de recommandation
### 2.1 Classification de géo-pertinence
**Décision** : 3 types de contenus selon leur pertinence géographique
| Type | Description | Exemple | Pondération géo |
|------|-------------|---------|-----------------|
| **Géo-ancré** | Contenu lié à un lieu précis | Audio-guide monument, pub restaurant local | 70% |
| **Géo-contextuel** | Pertinent dans une zone | Actualité régionale, événement local | 50% |
| **Géo-neutre** | Universel, pas de lien géo | Podcast philosophie, musique | 20% |
**Qui décide** :
- ✅ Créateur choisit le type à la publication
- ✅ Modération peut reclassifier après validation
- ✅ Modification possible après publication (tout le monde a le droit de se tromper)
**Justification** :
- Différencie audio-guide (hyper-local) des podcasts génériques
- Algorithme adapte automatiquement la pondération
- Coût : champ supplémentaire en DB + règle algo
---
### 2.2 Formule de scoring
**Décision** : Score combiné dynamique selon type de contenu
```
score_final = (score_geo * poids_geo_type)
+ (score_interets * poids_interets_type)
+ (score_engagement * 0.2)
+ (bonus_aleatoire)
où :
- score_geo = 1 - (distance_km / distance_max_km)
- score_interets = moyenne des jauges utilisateur pour les tags du contenu
- score_engagement = (taux_completion * 0.5) + (ratio_likes * 0.3) + (ratio_abonnements * 0.2)
- bonus_aleatoire = 10% des recommandations tirées aléatoirement
```
#### Calcul détaillé du score_interets
**Domaine des données** :
- Jauges utilisateur : stockées en pourcentage [0-100]
- score_interets : normalisé dans l'intervalle [0.0-1.0] pour pondération
**Formule exacte** :
```
score_interets = (SUM(gauge_values_for_tags) / NB_TAGS) / 100
où :
- gauge_values_for_tags = valeurs des jauges correspondant aux tags du contenu
- NB_TAGS = nombre de tags du contenu (minimum 1, maximum 3)
- Division par 100 pour normaliser [0-100] → [0.0-1.0]
```
**Exemple concret** :
```
Contenu : "Visite du Louvre"
Tags : ["Musique", "Tourisme"]
Utilisateur :
- Jauge "Musique" = 75%
- Jauge "Tourisme" = 60%
- Jauge "Automobile" = 40% (non pertinente, ignorée)
Calcul :
score_interets = ((75 + 60) / 2) / 100
= (135 / 2) / 100
= 67.5 / 100
= 0.675
Impact dans le scoring final (type géo-contextuel) :
score_final = (score_geo * 0.5) + (score_interets * 0.3) + (score_engagement * 0.2) + bonus_aleatoire
= (0.8 * 0.5) + (0.675 * 0.3) + (0.5 * 0.2) + 0
= 0.4 + 0.2025 + 0.1
= 0.7025 / 1.0
```
**Cas limites** :
- Utilisateur n'a aucune jauge pour les tags du contenu → score_interets = 0.5 (valeur neutre par défaut)
- Contenu avec 1 seul tag → score_interets = gauge_value / 100
- Jauges multiples → moyenne arithmétique simple (pas de pondération différente par tag)
- **Score géo excellent MAIS intérêts nuls** : Le contenu peut quand même être recommandé grâce à la pondération géographique. Exemple : contenu géo-ancré à 100m avec score_geo=1.0 et score_interets=0.0 obtient score_final = (1.0 × 0.7) + (0.0 × 0.1) + engagement = 0.7 + engagement. Ce comportement est accepté pour MVP car (1) le quota 6 contenus géolocalisés/h protège du spam, (2) l'info peut être utile contextuellement même sans intérêt marqué, (3) la distinction info/divertissement est reportée post-MVP.
**Pondérations par type** :
| Type | Poids géo | Poids intérêts |
|------|-----------|----------------|
| Géo-ancré | 0.7 | 0.1 |
| Géo-contextuel | 0.5 | 0.3 |
| Géo-neutre | 0.2 | 0.6 |
**Paramètres** :
- Distance max recommandée : **200 km**
- Dégradation : **linéaire** (1 - distance/200km)
- Rayon point GPS : **500m** (adapté au volume de contenu local)
**Tous ces paramètres sont configurables à chaud via interface admin.**
**Justification** :
- Flexibilité totale selon type de contenu
- Linéaire = rattrapage naturel du contenu viral ancien
- Auditable via métriques engagement (moyenne/médiane)
---
### 2.3 Score d'engagement et popularité
**Décision** : Intégration popularité avec poids 0.2
**Métriques** :
- **Taux de complétion** : écoutes >80% / total écoutes pertinentes (poids 0.5)
- **Ratio likes** : likes / écoutes (poids 0.3)
- **Ratio abonnements** : nouveaux abonnés après écoute / écoutes (poids 0.2)
**Distinction sources et abonnements** (neutralisation pénalités) :
Les métriques d'engagement **ne comptent que les écoutes pertinentes** pour éviter de pénaliser injustement les créateurs :
| Source écoute | Abonné au créateur ? | Skip <10s pénalise ? | Compte dans "total écoutes" ? | Justification |
|---------------|---------------------|---------------------|------------------------------|---------------|
| **`recommendation`** | ❌ Non | ✅ Oui | ✅ Oui | Skip = mauvaise recommandation OU mauvais contenu |
| **`recommendation`** | ✅ Oui | ❌ **Non** | ❌ **Non** | Abonné intéressé globalement, skip contextuel |
| **`search`** | Peu importe | ❌ Non | ❌ Non | User cherchait quelque chose de précis, skip = "pas maintenant" |
| **`direct_link`** | Peu importe | ❌ Non | ❌ Non | User curieux, peut skip sans jugement qualité |
| **`profile`** | Peu importe | ❌ Non | ❌ Non | User explore catalogue créateur |
| **`history`** | Peu importe | ❌ Non | ❌ Non | Pas une première écoute |
| **`live_notification`** | ❌ Non | ✅ Oui | ✅ Oui | Abonné normalement intéressé |
| **`live_notification`** | ✅ Oui | ❌ **Non** | ❌ **Non** | Abonné = affinité, skip contextuel |
| **`audio_guide`** | Peu importe | ❌ Non | ❌ Non | Navigation guidée, pas jugement qualité |
**Calcul engagement créateur** (exemple SQL) :
```sql
SELECT
content_id,
AVG(completion_rate) as avg_completion,
COUNT(*) FILTER (WHERE completion_rate > 0.8) as complete_listens,
COUNT(*) FILTER (WHERE completion_rate < 0.1 AND NOT is_subscribed) as penalizing_skips
FROM user_listening_history
WHERE source IN ('recommendation', 'live_notification') -- Sources pertinentes
GROUP BY content_id;
```
**Seuil minimum** :
- Minimum **50 écoutes pertinentes** avant de considérer l'engagement
- Contenu <50 écoutes : score engagement = 0.5 (neutre)
**Contenu viral** :
- Un contenu viral à Paris **peut** être proposé à Marseille
- Score géo faible compensé par score engagement élevé
- Paramétrable admin
**Dépréciation temporelle** :
- Pas de dépréciation automatique
- Ratio linéaire = contenu ancien mais toujours apprécié reste pertinent
**Justification** :
- Équilibre découverte / qualité
- **Protection créateur** : abonnés fidèles ne pénalisent pas les métriques
- **Anti-raid naturel** : skips via search/direct_link ne comptent pas (raid inefficace)
- **Cohérence UX** : abonnement = signal d'affinité fort, skip ponctuel ≠ rejet créateur
- Pas de pénalisation arbitraire des contenus anciens
- Coût : calculs sur métriques existantes + colonne `source` + colonne `is_subscribed`
---
### 2.4 Part d'aléatoire (exploration)
**Décision** : 10% par défaut, paramétrable utilisateur
**Fonctionnement** :
- 1 contenu sur 10 = tirage aléatoire (hors historique déjà écouté)
- Utilisateur peut ajuster : curseur 0% (aucun aléatoire) à 50% (exploration max)
**Curseur utilisateur** :
- 🎯 **0%** : Personnalisé max (recommandations strictes)
- ⚖️ **10%** : Équilibré (défaut)
- 🎲 **30%** : Découverte élevée
- 🌍 **50%** : Découverte max (équivaut à national = découverte)
**Justification** :
- Évite la bulle de filtre
- Laisse l'utilisateur maître de son expérience
- Coût : variable aléatoire en algo
---
### 2.5 Contenu politique (version MVP simplifiée)
> ⚠️ **Note** : La classification politique avancée (échelle gauche/droite, équilibrage imposé) a été reportée post-MVP. Voir [ANNEXE-POST-MVP.md](ANNEXE-POST-MVP.md) pour la version complète.
**Décision MVP** : Tag simple "Politique" sans classification idéologique
**Tagging** :
- Créateur peut taguer son contenu comme "Politique" (optionnel)
- Tag "Politique" au même niveau que "Économie", "Sport", "Culture", etc.
- **Pas de classification gauche/droite**
- **Pas d'équilibrage imposé**
**Filtrage utilisateur** :
- Option paramètres : **"Masquer contenu politique"**
- Si activé → 0% de contenus tagués "Politique" dans le feed
- Par défaut : désactivé (tous contenus visibles)
**Justification MVP** :
- **Simplicité** : Pas de modération politique coûteuse (~2000€/mois économisés)
- **Neutralité technique** : Aucun jugement éditorial sur orientation
- **Risque minimal** : Évite controverses et contentieux DSA au lancement
- **Fonctionnel** : Utilisateurs peuvent filtrer si souhaité
**Post-MVP** :
- Classification avancée possible si forte demande utilisateurs
- Nécessite ressources modération dédiées et audit DSA
---
### 2.6 Mode Kids (13-15 ans)
**Décision** : Mode optionnel pour adolescents 13-15 ans uniquement
> ⚠️ **Note** : Âge minimum d'inscription = **13 ans** (obligation légale EU). Pas d'utilisateurs <13 ans sur la plateforme.
**Tranche concernée** :
| Tranche | Description | Contenus autorisés | Restrictions |
|---------|-------------|-------------------|--------------|
| **13-15 ans** | Collège | Contenus "Tous publics" uniquement | Filtrage 16+ et 18+ |
**Activation** :
-**Pas d'activation automatique** (tous les utilisateurs ont ≥13 ans)
-**Activation manuelle** via toggle paramètres
- ✅ Parents peuvent activer pour leurs enfants 13-15 ans
- ✅ Utilisateur peut désactiver à tout moment
**Filtrage quand Mode Kids activé** :
- ✅ Contenus "Tous publics" uniquement
- ❌ Exclusion contenus 16+ et 18+
- ❌ Pas de contenu politique (automatiquement filtré)
- ❌ Pas de publicité (ou uniquement pub validée manuellement)
**Interface** :
- Interface standard (pas d'interface dédiée enfants pour MVP)
- Filtrage algorithmique des contenus inappropriés
**Justification** :
- **Conformité légale** : Âge minimum 13 ans (RGPD, DSA)
- **Simplicité MVP** : Un seul mode optionnel vs 4 tranches d'âge
- **Protection mineurs** : Filtrage contenus adultes pour 13-15 ans
- **Flexibilité** : Parents décident d'activer ou non
---
### 2.7 Déclenchement géographique
**Décision** : Notification au passage, pas d'anticipation
**Fonctionnement** :
1. Utilisateur passe à <500m d'un point GPS (contenu géo-ancré)
2. **Notification sonore** (bip court) + **visuelle** (logo selon type)
3. Types de logos : 📍 Info, 🏛️ Culturel, 🍴 Commercial, 🎭 Événement
4. Délai réaction utilisateur : **5 secondes** pour accepter (bouton volant ou commande vocale)
5. Si accepté → lecture immédiate
6. Si ignoré → contenu proposé normalement en file d'attente
**Publicités** :
- ⚠️ **Jamais d'interruption** de contenu en cours
- Pub s'intercale **entre deux séquences** uniquement
- Notification pub : son différent (facultatif selon paramètres)
**Gestion demi-tour** :
- Si utilisateur repart du point après notification → pas de nouvelle notification (déjà proposé)
- Réinitialisation après 24h
**Justification** :
- Respect écoute en cours (pas de coupure brutale)
- UX fluide (utilisateur garde contrôle)
- Simplicité technique (pas de prédiction trajectoire)
---
### 2.8 Historique et repropositon
**Décision** : Pas de reproposition sauf contenu partiel ou skip d'abonné
**Règles** :
| État écoute | Completion | Abonné au créateur ? | Action |
|-------------|------------|---------------------|--------|
| **Écouté complètement** | >80% | Peu importe | ❌ Ne jamais reproposer (sauf flag `replayable = true` pour audio-guides) |
| **Skippé rapidement** | <10s | ❌ Non | ❌ Ne pas reproposer (signal négatif clair) |
| **Skippé rapidement** | <10s | ✅ **Oui** | ✅ **Peut reproposer** (abonnement = affinité, skip contextuel) |
| **Partiellement écouté** | 10-80% | Peu importe | ✅ Reproposer avec reprise position (`last_position_seconds`) |
**Stockage historique** :
- Table `user_content_history` (user_id, content_id, creator_id, **is_subscribed**, completion_rate, last_position, listened_at)
- Historique **illimité** (PostgreSQL)
- Algorithme considère les **100 derniers** pour optimisation requêtes
- Export complet disponible (RGPD)
**Colonne `is_subscribed`** :
- Booléen stockant si l'utilisateur était abonné au créateur **au moment de l'écoute**
- Permet de distinguer les skips d'abonnés (contextuels) des skips de non-abonnés (désintérêt)
- Utilisé pour décisions de reproposition et calculs d'engagement
**Justification** :
- Découverte maximale (pas de redites)
- **Cohérence abonnement** : un skip ponctuel d'un abonné ≠ rejet du créateur (peut être contextuel : "pas maintenant", "pas ce sujet", "mauvais timing")
- Respect erreurs de clic (contenu partiel = 2nde chance)
- Coût stockage négligeable (PostgreSQL scalable)
---
### 2.9 Paramétrabilité admin (interface dashboard)
**Décision** : Tous paramètres scoring exposés + A/B testing
**Paramètres configurables à chaud** :
| Paramètre | Plage | Défaut | Unité |
|-----------|-------|--------|-------|
| `poids_geo_ancre` | 0.5 - 1.0 | 0.7 | % |
| `poids_geo_contextuel` | 0.3 - 0.7 | 0.5 | % |
| `poids_geo_neutre` | 0.0 - 0.4 | 0.2 | % |
| `poids_engagement` | 0.0 - 0.5 | 0.2 | % |
| `part_aleatoire_global` | 0.0 - 0.3 | 0.1 | % |
| `distance_max_km` | 50 - 500 | 200 | km |
| `rayon_gps_point_m` | 100 - 2000 | 500 | m |
| `seuil_min_ecoutes_engagement` | 10 - 200 | 50 | nb |
**Application changements** :
- Immédiat : nouveaux calculs utilisent nouvelle config
- Aucun recalcul batch (coût CPU)
- Version config trackée (git-like)
- Rollback 1 clic
**A/B Testing** :
- Création variantes (Config A vs Config B)
- Split utilisateurs 50/50 aléatoire
- Métriques comparatives : taux complétion, engagement, session duration
- Dashboard graphique temps réel
**Audit engagement** :
- Métriques clés : moyenne/médiane temps d'écoute par session
- Graphiques : évolution engagement selon config
- Export CSV pour analyse externe
**Justification** :
- Optimisation continue sans redéploiement
- Data-driven decisions (métriques objectives)
- Coût : dashboard admin à développer (one-time)
---
### 2.10 Paramétrabilité utilisateur
**Décision** : Curseurs avancés avec profils sauvegardables
**Niveaux de personnalisation** :
**Curseurs disponibles** :
- 📍 **Géolocalisation** : Local ← slider → National (découverte = national)
- 🎲 **Découverte** : 0% ← slider → 50% (part aléatoire)
- ⚖️ **Politique** : Masquer / Équilibré / Mes préférences
**Profils sauvegardables** :
- 🚗 Trajet quotidien (boulot) : géo local, découverte 5%, politique masqué
- 🛣️ Road trip : géo régional, découverte 30%, politique équilibré
- 👶 Enfants : Mode Kids activé
**Synchronisation** :
- ✅ Sync profils entre devices (cloud PostgreSQL)
- ❌ Pas de partage profils entre utilisateurs (famille)
- Auto-switch selon context (détection trajet récurrent via GPS)
**Sécurité conduite** :
- ⚠️ **Blocage modification si vitesse GPS >10 km/h**
- Warning au lancement app : "Configurez avant de prendre la route"
- Modifications uniquement app arrêtée/passager
**Justification** :
- Utilisateur maître de son expérience
- Contextes d'usage différents (quotidien vs voyage)
- Sécurité routière (pas de distraction)
---
### 2.11 Médias traditionnels
**Décision** : Ouverture aux médias établis
**Médias autorisés** :
- Presse nationale : Le Monde, Le Parisien, Libération, Le Figaro, etc.
- Radios : France Inter, RTL, Europe 1, etc.
- Médias régionaux : Ouest-France, Sud-Ouest, etc.
**Format contenus** :
- Flashs info géolocalisés (actualité régionale)
- Chroniques thématiques (culture, économie, sport)
- Éditos et débats (classification politique appliquée)
**Validation** :
- Compte média vérifié (badge ✓)
- Pas de validation 3 premiers contenus (confiance établie)
- Modération a posteriori uniquement
**Monétisation** :
- Partage revenus pub standard (même conditions créateurs)
- Possibilité sponsoring direct (pas via plateforme)
**Justification** :
- Crédibilité plateforme (contenus professionnels)
- Diversité éditoriale
- Attractivité grand public (noms reconnus)
---
## Récapitulatif Section 2

View File

@@ -0,0 +1,160 @@
## 3. Centres d'intérêt et jauges
### 3.1 Évolution des jauges
**Décision** : Système simple avec valeurs fixes (points de pourcentage absolus)
| Action | Impact jauge | Justification |
|--------|--------------|---------------|
| **Like automatique renforcé (≥80% écoute)** | **+2%** | Signal fort d'intérêt (écoute quasi-complète) |
| **Like automatique standard (30-79% écoute)** | **+1%** | Signal modéré d'intérêt |
| **Like explicite (manuel)** | **+2%** | Signal fort, cumulable avec auto |
| **Abonnement créateur** | **+5%** sur tous ses tags | Signal très fort d'affinité |
| **Skip rapide (<10s), NON abonné** | **-0.5%** | Désintérêt marqué (signal négatif légitime) |
| **Skip rapide (<10s), ABONNÉ au créateur** | **0%** (neutre) | Abonnement = affinité forte, skip contextuel (pas ce contenu spécifique) |
| **Skip tardif (≥30%)** | **0%** | Neutre (contenu essayé suffisamment) |
**Note importante** : Les pourcentages indiqués sont des **points de pourcentage absolus**, **PAS des pourcentages relatifs**.
**Calcul** :
- Si jauge "Automobile" = 45%
- Like renforcé (+2%) → 45 + 2 = **47%**
- **NOT** 45 × 1.02 = 45.9% ❌
Cette approche garantit une **progression linéaire** et **équitable** pour tous les utilisateurs, indépendamment de leur niveau actuel dans une jauge.
**Paramètres techniques** :
- Les jauges sont bornées strictement entre **0% et 100%**
- Calcul immédiat à chaque action (pas de batch différé)
- Les tags du contenu sont définis par le créateur à la publication
- Si un contenu a plusieurs tags, chaque jauge correspondante est impactée
**Exemple de calcul** :
```
Contenu de 5 minutes tagué "Automobile" + "Voyage"
Scénario 1 : Écoute 4min30 (90%)
→ Like automatique renforcé (+2%)
→ Jauge Automobile : 45% → 47%
→ Jauge Voyage : 60% → 62%
Scénario 2 : Écoute 2min30 (50%)
→ Like automatique standard (+1%)
→ Jauge Automobile : 45% → 46%
→ Jauge Voyage : 60% → 61%
Scénario 3 : Écoute 2min30 (50%) + Like manuel
→ Like auto +1% puis like manuel +2% = +3% total
→ Jauge Automobile : 45% → 48%
→ Jauge Voyage : 60% → 63%
Scénario 4 : Skip après 5s (NON abonné au créateur)
→ Signal négatif (-0.5%)
→ Jauge Automobile : 45% → 44.5%
→ Jauge Voyage : 60% → 59.5%
Scénario 5 : Skip après 5s (ABONNÉ au créateur)
→ Neutre (0%, pas de pénalité)
→ Jauge Automobile : 45% → 45%
→ Jauge Voyage : 60% → 60%
→ Raison : Abonnement signale affinité globale, skip ponctuel = pas intéressé par CE contenu spécifique
```
**Justification** :
- **Like automatique** : Reflète l'engagement réel (voir [Règle 05 - Section 5.3](05-interactions-navigation.md#53-interactions-au-volant--like-automatique-et-engagement))
- **Sécurité routière** : Pas d'action complexe en conduite
- **Prévisibilité** : Règles claires et déterministes
- **Progression linéaire** : Évite l'effet "rich get richer" (progression équitable)
- **Coût minimal** : Calculs simples en backend (addition/soustraction uniquement)
- **Fiabilité** : Pas d'edge cases complexes (pas de risque d'overflow avec multiplication)
- **Ajustable** : Valeurs modifiables via dashboard admin si besoin
> 📋 **Référence technique** : Voir [Règle 05 - Implémentation Technique](05-interactions-navigation.md#implémentation-technique-backend) pour l'architecture backend détaillée.
---
### 3.2 Jauge initiale
**Décision** : Démarrage neutre à 50%, pas de questionnaire
**À l'inscription** :
- Toutes les jauges d'intérêt sont initialisées à **50%**
- Pas de questionnaire onboarding (friction zéro)
- L'algorithme apprend naturellement via les premières écoutes
**Catégories disponibles** :
- Automobile
- Voyage
- Famille
- Amour
- Musique
- Économie
- Cryptomonnaie
- Politique
- Culture générale
- Sport
- Technologie
- Santé
- *... (extensible)*
**Cold start (premiers jours)** :
1. Nouvel utilisateur s'inscrit → toutes jauges à 50%
2. Écoute premier podcast "Automobile" → jauge Auto monte à 51%
3. Skip un contenu "Économie" → jauge Éco descend à 48%
4. Après 10-15 écoutes, profil commence à se dessiner clairement
**Alternative optionnelle (post-MVP)** :
- Questionnaire **optionnel** proposé après 3 écoutes (in-app)
- Message : "Améliorez vos recommandations en sélectionnant vos centres d'intérêt"
- Si rempli : jauges sélectionnées passent à 70%, non sélectionnées à 30%
- Si skip : conserve 50% partout
**Justification** :
- **Inscription ultra-rapide** : pas de questionnaire = moins de churn
- **Découverte naturelle** : l'algorithme apprend en quelques écoutes
- **Équitable** : pas de biais initial vers certains créateurs
- **Comportement déterministe** : facile à tester et débugger
- **Cold start acceptable** : à 50%, tous les contenus ont une chance égale initialement
---
### 3.3 Dégradation temporelle
**Décision** : Pas de dégradation automatique
Les jauges **ne diminuent jamais** avec le temps de manière automatique.
**Règle** :
- Une jauge ne change **que par les actions utilisateur** (like, écoute, skip)
- Pas de cron job de dégradation périodique
- Pas de "rafraîchissement" artificiel
**Scénario illustratif** :
```
Utilisateur aimait "Économie" (jauge 80%) il y a 1 an
→ Depuis, skip tous les contenus Éco
→ Jauge descend naturellement à 40% via les skips
→ Pas besoin de dégradation temporelle
```
**Si utilisateur inactif longtemps** :
- Utilisateur part en vacances 6 mois → jauges conservées
- Au retour : ses jauges reflètent toujours ses goûts d'avant
- Comportement cohérent et prévisible
**Alternative utilisateur (contrôle explicite)** :
- Bouton "Réinitialiser mes centres d'intérêt" dans paramètres
- Action manuelle : remet toutes les jauges à 50%
- Permet nouveau départ si souhaité (changement de vie, etc.)
**Justification** :
- **Principe KISS** (Keep It Simple, Stupid)
- **Coût 0** : pas de batch nocturne, pas de calculs temporels
- **Fiabilité maximale** : pas de bugs de fuseaux horaires, dates, etc.
- **UX prévisible** : jauge = reflet des actions, pas d'automatisme caché
- **Respect historique** : si utilisateur aimait X depuis 2 ans, pourquoi "oublier" ?
- **Évolution naturelle** : les actions récentes suffisent à faire évoluer les jauges
---
## Récapitulatif Section 3

View File

@@ -0,0 +1,574 @@
## 5. Interactions et navigation
### 5.1 File d'attente et commande "Suivant"
**Décision** : Pré-calcul 5 contenus avec insertion prioritaire pour points géographiques
**File d'attente** :
- **5 contenus pré-calculés** en cache (Redis)
- Recalcul automatique si :
- Déplacement >10km
- Toutes les 10 minutes (rafraîchissement contenu)
- File d'attente <3 contenus restants
**Insertion prioritaire géo-ancrée (mode voiture uniquement)** :
**Détection** :
- Calcul ETA (Estimated Time of Arrival) via API GPS native iOS/Android
- Notification déclenchée **7 secondes avant** d'arriver au point GPS
- Si vitesse < 5 km/h ET distance < 50m → notification immédiate
- ⚠️ **App doit être ouverte** (pas de détection en arrière-plan en mode voiture)
**Notification** :
- **Sonore uniquement** : bip court ou son personnalisé RoadWave
- **Visuelle minimale** : icône selon type de contenu (🏛️ culture, 👨‍👩‍👧 famille, 🎵 musique, etc.)
- **Compteur visible** : 7...6...5...4...3...2...1 (décompte des secondes)
- **Pas de texte affiché** (éviter distraction conducteur)
- **Pas de bouton "Annuler"** : seul le bouton "Suivant" permet validation
**Actions utilisateur** :
1. User entend notification sonore + voit icône et compteur
2. User appuie "Suivant" dans les 7 secondes → décompte 5s démarre
3. Pendant décompte : contenu actuel continue, compteur visible (5...4...3...2...1)
4. Si contenu actuel se termine pendant décompte → contenu suivant du buffer démarre
5. À la fin du décompte → contenu géolocalisé démarre (fade out/in 0.3s)
**Si user n'appuie pas sur "Suivant"** :
- Notification disparaît après 7 secondes
- Contenu géolocalisé est perdu (pas d'insertion dans file)
- Pas de nouveau contenu géolocalisé pendant **10 minutes** (éviter spam)
**Limitation anti-spam** :
- Maximum **6 contenus géolocalisés par heure**
- Timer reset toutes les heures (rolling window)
- Exception : séquences d'un même audio-guide multi-séquences (comptent comme 1)
- Si quota atteint : notifications suivantes ignorées jusqu'à libération du quota
**Invalidation immédiate** :
- Utilisateur change ses préférences (curseurs géo/découverte/politique)
- ⚠️ **Modification bloquée si vitesse GPS >10 km/h** (sécurité routière)
- Live démarre d'un créateur suivi dans la zone
**Implémentation** :
```
Redis cache :
- Clé : user:{user_id}:queue
- Structure : [content_1, content_2, ..., content_5]
- Métadonnées : {last_lat, last_lon, computed_at, mode: "voiture"|"pieton"}
- TTL : 15 minutes
Tracking GPS temps réel (mobile) :
- Vérification toutes les 1 seconde
- Calcul ETA vers points géolocalisés proches (rayon 500m)
- Si ETA ≤ 7s → trigger notification
- Historique GPS : 30 derniers points pour calcul vitesse moyenne
Quota anti-spam (Redis) :
- Clé : user:{user_id}:geo_quota
- Structure : sorted set avec timestamps des 6 derniers contenus
- TTL : 1 heure
- Vérification avant notification : ZCOUNT pour compter contenus dernière heure
Cooldown après ignorance (Redis) :
- Clé : user:{user_id}:geo_cooldown
- TTL : 10 minutes
- Set après notification ignorée
```
**Justification** :
- **Expérience fluide** : pas de latence au clic "Suivant"
- **Réactivité géo** : contenu local inséré immédiatement
- **Coût optimisé** : recalcul uniquement si nécessaire
- **Sécurité** : pas de modification en conduite
---
### 5.1.2 Mode piéton (audio-guides)
**Décision** : Notifications push en arrière-plan avec rayon large
**Contexte** :
- Mode piéton détecté automatiquement si vitesse moyenne < 5 km/h
- Cas d'usage : visites à pied, musées, monuments, quartiers historiques
- User n'a pas besoin d'avoir l'app ouverte
- ⚠️ **Fonctionnalité optionnelle** : requiert permission "localisation en arrière-plan" (activée par user)
**Détection** :
- App peut être en arrière-plan (si permission accordée)
- Rayon de détection : **200 mètres** autour du point GPS
- Geofencing iOS/Android pour minimiser consommation batterie
- Permission demandée uniquement si user active "Notifications audio-guides piéton" dans settings
**Notification push système** :
Format :
```
Titre : "Audio-guide à proximité"
Body : "[Nom du contenu] - [Nom créateur]"
Action : Tap → ouvre app sur le contenu
```
Exemple :
```
Audio-guide à proximité
Musée du Louvre : La Joconde - @paris_museum
```
**Permissions requises** :
⚠️ **Important** : RoadWave utilise une **stratégie de permissions progressive** pour maximiser l'acceptation utilisateur et la validation stores.
**Étape 1 - Permission de base (tous utilisateurs)** :
- iOS : "Allow While Using App" (`locationWhenInUse`)
- Android : `ACCESS_FINE_LOCATION`
- **Demandée** : Au premier lancement (onboarding)
- **Permet** : Mode voiture complet ✅
**Étape 2 - Permission arrière-plan (optionnelle, mode piéton uniquement)** :
- iOS : "Allow Always" (`locationAlways`)
- Android : `ACCESS_BACKGROUND_LOCATION`
- **Demandée** : Uniquement si user active "Notifications audio-guides piéton" dans settings
- **Précédée** : Écran d'éducation expliquant l'usage (requis stores)
- **Permet** : Mode piéton avec notifications push en arrière-plan ✅
**Si permission arrière-plan refusée** :
- Mode piéton **désactivé** (toggle grisé dans settings)
- Mode voiture reste **pleinement fonctionnel**
- Audio-guides accessibles en mode **manuel** (user ouvre app, lance contenu)
- **Garantie RGPD** : App utilisable sans permission arrière-plan ✅
iOS (`Info.plist`) :
```xml
<key>NSLocationWhenInUseUsageDescription</key>
<string>RoadWave utilise votre position pour vous proposer des contenus audio géolocalisés adaptés à votre trajet en temps réel.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Si vous activez les notifications audio-guides piéton, RoadWave peut vous alerter lorsque vous passez près d'un monument ou musée, même quand l'app est en arrière-plan. Cette fonctionnalité est optionnelle et peut être désactivée à tout moment dans les réglages.</string>
```
Android (`AndroidManifest.xml`) :
```xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
```
> 📋 **Référence technique** : Voir [ADR-010 - Stratégie de Permissions](../adr/010-frontend-mobile.md#stratégie-de-permissions-iosandroid) pour détails d'implémentation.
**Disclosure avant demande permission** (Android requis, iOS recommandé) :
Écran affiché avant demande permission "Always Location" :
```
┌────────────────────────────────────────┐
│ 📍 Notifications audio-guides piéton │
├────────────────────────────────────────┤
│ Pour vous alerter d'audio-guides à │
│ proximité même quand vous marchez avec │
│ l'app fermée, RoadWave a besoin de │
│ votre position en arrière-plan. │
│ │
│ Votre position sera utilisée pour : │
│ ✅ Détecter monuments à 200m │
│ ✅ Vous envoyer une notification │
│ │
│ Votre position ne sera jamais : │
│ ❌ Vendue à des tiers │
│ ❌ Utilisée pour de la publicité │
│ │
│ Cette fonctionnalité est optionnelle. │
│ Vous pouvez utiliser RoadWave sans │
│ cette permission. │
│ │
│ [Continuer] [Non merci] │
│ │
│ Plus d'infos : Politique confidentialité│
└────────────────────────────────────────┘
```
**Si user refuse** :
- Mode piéton désactivé (uniquement mode voiture disponible)
- App fonctionne normalement avec permission "When In Use"
- Audio-guides accessibles en mode manuel (user ouvre app, sélectionne contenu)
**Comportement après tap sur notification** :
1. User tap notification push
2. App s'ouvre sur la page du contenu
3. User peut démarrer la lecture manuellement
4. Navigation libre (voir section 16.2 pour audio-guides piéton)
**Basculement automatique voiture ↔ piéton** :
Détection par vitesse GPS moyenne sur 30 secondes :
- Vitesse < 5 km/h (stable 10s) → mode piéton
- Vitesse ≥ 5 km/h (stable 10s) → mode voiture
Changements de mode :
| Mode actuel | Vitesse détectée | Nouveau mode | Effet |
|-------------|------------------|--------------|-------|
| Piéton | ≥ 5 km/h | Voiture | Notifications push → sonores + icône (app ouverte requise) |
| Voiture | < 5 km/h | Piéton | Notifications sonores → push arrière-plan |
**Pas de popup confirmation** :
- Basculement transparent et automatique
- User n'a rien à faire
- Hysteresis (10s) pour éviter basculements intempestifs
**Quota anti-spam mode piéton** :
- Même limitation que mode voiture : **6 contenus/heure**
- Cooldown 10 min si notification ignorée (app pas ouverte après tap)
**Justification** :
- ✅ Expérience adaptée aux visites à pied (rayon large, pas de timing précis)
- ✅ Économie batterie (geofencing natif iOS/Android)
- ✅ User peut garder téléphone en poche
- ✅ Basculement automatique = pas de friction
---
### 5.2 Commande "Précédent"
**Décision** : Comportement smart selon progression écoute
**Règles** :
| Situation | Temps écouté | Action "Précédent" |
|-----------|--------------|-------------------|
| **Début de contenu** | <10 secondes | Retour au contenu précédent (position exacte) |
| **Milieu/fin** | ≥10 secondes | Replay contenu actuel depuis le début |
| **Premier de session** | N/A | Replay depuis début (rien avant) |
**Historique de navigation** :
- **10 contenus maximum** en mémoire (Redis List)
- Structure : `[{content_id, position_seconds, listened_at}, ...]`
- FIFO : au-delà de 10, suppression du plus ancien
**Exemple scénario** :
```
Utilisateur écoute :
1. Contenu A → écoute 5s → "Suivant"
2. Contenu B → écoute 2min30 → "Suivant"
3. Contenu C → écoute 5s → "Précédent"
→ Retour Contenu B à 2min30 (car >10s)
4. Sur Contenu B → "Précédent"
→ Retour Contenu A à 5s (position exacte)
```
**Interface (responsabilité front)** :
- ❌ Pas de message UI
- ✅ Progress bar revient au début ou à position exacte
- ✅ Animation fluide (transition 0.3s)
**Justification** :
- **UX intuitive** : comportement standard Spotify/YouTube
- **Pas de frustration** : si début, vraiment revenir en arrière
- **Simplicité** : règle unique (seuil 10s)
---
### 5.3 Interactions au volant : Like automatique et engagement
**Décision** : Like automatique basé sur le temps d'écoute
**Problème technique identifié** :
- iOS et Android ne supportent **pas nativement** les appuis longs ou doubles-appuis sur les commandes média
- Les commandes physiques au volant varient selon les véhicules (pas de bouton "Pause" dédié sur beaucoup de modèles)
- Système de double-appui/appui long = **non-intuitif** et **risques sécurité** (regarder écran pour feedback)
---
#### Commandes au volant simplifiées
**Actions disponibles** (100% compatibles tous véhicules) :
| Commande physique | Action RoadWave |
|-------------------|-----------------|
| **Suivant** | Passer au contenu suivant |
| **Précédent** | Revenir au contenu précédent (règle 10s, voir section 5.2) |
| **Play/Pause** | Pause/reprise lecture (fade out 0.3s) |
**Aucune action complexe au volant** → Sécurité routière maximale.
---
#### Like automatique implicite
**Principe** : Le système détecte automatiquement l'intérêt utilisateur selon le temps d'écoute.
**Règles d'attribution** :
| Durée écoutée | Action automatique | Points jauge | Justification |
|---------------|-------------------|--------------|---------------|
| **≥ 80% du contenu** | Like renforcé | +2.0 | Écoute quasi-complète = fort intérêt |
| **30-79% du contenu** | Like standard | +1.0 | Écoute significative = intérêt |
| **< 30% du contenu** | Pas de like | 0 | Écoute trop courte |
| **Skip après <10s** | Signal négatif | -0.5 | Désintérêt marqué |
**Exemples concrets** :
```
Contenu de 3 minutes (180s) :
- Écoute 2min30 (83%) → Like renforcé (+2 points)
- Écoute 1min15 (42%) → Like standard (+1 point)
- Écoute 30s (17%) puis skip → Pas de like
- Skip après 5s → Signal négatif (-0.5 point)
Contenu de 15 minutes (900s) :
- Écoute 13min (87%) → Like renforcé (+2 points)
- Écoute 6min (40%) → Like standard (+1 point)
```
---
#### Actions complémentaires (mode piéton uniquement)
**Interface mobile** (vitesse < 5 km/h) :
| Action | Moyen | Effet |
|--------|-------|-------|
| **Like explicite** | Bouton cœur | +2 points jauge (même si déjà liké auto) |
| **Unlike** | Re-clic cœur (toggle) | -2 points jauge |
| **Abonnement** | Bouton "S'abonner" profil créateur | +5 points toutes jauges tags créateur |
| **Désabonnement** | Bouton "Se désabonner" | -5 points |
| **Signalement** | Menu contextuel "⋮" | Ouverture flux modération |
**Feedback visuel** :
- **Like automatique** : Badge discret "♥ Ajouté à vos favoris" (2s, bas de l'écran)
- **Like explicite** : Animation cœur rouge + vibration courte
- **Abonnement** : Animation étoile dorée + badge "Abonné ✓"
**Disponibilité** :
- ✅ Mode piéton (vitesse < 5 km/h) : toutes les actions disponibles
- ❌ Mode voiture (vitesse ≥ 5 km/h) : aucune de ces actions (sauf like automatique)
---
#### Gestion impacts jauges (algorithme)
**Like automatique** :
- Like renforcé (≥80%) → **+2% jauges** de tous les tags du contenu
- Like standard (30-79%) → **+1% jauges** des tags du contenu
- Signal négatif (skip <10s) → **-0.5% jauges** des tags du contenu
**Actions explicites** :
- Like manuel → **+2% jauges** (cumulable avec like auto)
- Unlike → **-2% jauges**
- Abonnement → **+5% toutes jauges** tags créateur
- Désabonnement → **-5% toutes jauges**
**Persistance** :
- Événements stockés en base (table `listen_events`)
- Mise à jour jauges : **immédiate** (Redis) + **async batch** (PostgreSQL)
---
#### Implémentation technique
**Backend** (Go) :
```go
type ListenEvent struct {
UserID string
ContentID string
StartedAt time.Time
StoppedAt time.Time
Duration int // secondes écoutées
ContentTotal int // durée totale contenu
Percentage float64 // duration / contentTotal * 100
Action string // "completed", "skipped", "paused"
}
func ProcessListenEvent(event ListenEvent) {
percentage := event.Percentage
// Signal négatif fort
if event.Action == "skipped" && event.Duration < 10 {
UpdateJauges(event.UserID, event.ContentID, -0.5)
return
}
// Like automatique
if percentage >= 80 {
AutoLike(event.UserID, event.ContentID, 2.0) // Renforcé
} else if percentage >= 30 {
AutoLike(event.UserID, event.ContentID, 1.0) // Standard
}
// < 30% : pas de like
}
```
**Mobile** (iOS/Android) :
```swift
// iOS - Tracking écoute
class AudioPlayerManager {
var startTime: Date?
let contentDuration: TimeInterval
func onPlay() {
startTime = Date()
}
func onStop(action: String) { // "completed" | "skipped" | "paused"
guard let start = startTime else { return }
let duration = Date().timeIntervalSince(start)
let percentage = (duration / contentDuration) * 100
// API call
API.track(ListenEvent(
contentId: currentContentId,
duration: Int(duration),
percentage: percentage,
action: action
))
}
}
```
---
#### Justification
**Avantages** :
-**Sécurité routière maximale** : aucune action complexe au volant
-**UX intuitive** : comportement standard industrie (Spotify, YouTube Music, Deezer)
-**Compatibilité 100%** : fonctionne sur tous véhicules, tous OS
-**Engagement amélioré** : tous les contenus écoutés génèrent des signaux
-**Algorithme plus précis** : données granulaires (30%, 50%, 80%, 100%)
-**Simplicité développement** : pas de workarounds complexes iOS/Android
**Inconvénients mitigés** :
- ⚠️ Pas de like explicite en conduite → **Mitigation** : like automatique + vocal (CarPlay/Android Auto)
- ⚠️ Pas d'abonnement en conduite → **Mitigation** : liste "Créateurs à découvrir" dans app
- ⚠️ Like automatique peut surprendre → **Mitigation** : onboarding clair + unlike possible
---
#### Communication utilisateurs (onboarding)
**Écran onboarding 1** :
```
🚗 Conduite sécurisée
RoadWave détecte automatiquement vos goûts
selon vos écoutes.
Plus vous écoutez longtemps, plus
l'algorithme s'améliore !
[Suivant]
```
**Écran onboarding 2** :
```
❤️ Likes automatiques
Pas besoin de liker manuellement :
si vous écoutez >50% d'un contenu,
on comprend que vous aimez !
[Suivant]
```
**Écran onboarding 3** :
```
⏸️ Commandes simples
Utilisez les boutons au volant :
• Suivant → Prochain contenu
• Précédent → Contenu d'avant
• Pause → Mettre en pause
[Commencer]
```
---
#### Implémentation Technique (Backend)
**Architecture** : 2 services séparés pour responsabilités distinctes
1. **Gauge Calculation Service** :
- Calcule l'ajustement basé sur % écoute (≥80% → +2%, 30-79% → +1%, skip <10s → -0.5%)
- Retourne un ajustement absolu (float64, en points de pourcentage)
- **Stateless** : logique métier pure, pas d'accès DB
- **Testable** : unitairement sans dépendances
2. **Gauge Update Service** :
- Applique l'ajustement aux jauges concernées (addition/soustraction)
- Applique les bornes [0, 100] (via fonction `clamp`)
- Gère les multi-tags : si contenu a N tags → mise à jour de N jauges
- Persiste : Redis (immédiat) + PostgreSQL (batch async)
**Séparation des responsabilités** :
-**Calculation** : Logique métier pure (réutilisable pour auto-like, skip, actions manuelles)
-**Update** : Persistance et contraintes techniques
**Pattern de calcul** :
```go
// ✅ CORRECT : Addition de points absolus
newValue := currentValue + adjustment
newValue = clamp(newValue, 0.0, 100.0)
// ❌ INCORRECT : Multiplication (pourcentage relatif)
newValue := currentValue * (1 + adjustment/100) // NE PAS FAIRE
```
**Persistance** :
- **Redis** : Mise à jour immédiate (latence <10ms)
- **PostgreSQL** : Batch async toutes les 5 minutes (cohérence finale)
- Raison : Jauges consultées fréquemment (recommandations temps réel)
---
### 5.4 Lecture en boucle et enchaînement
**Décision** : Passage automatique après 2s + insertion pub paramétrable
**Fin de contenu** :
1. Audio termine → **Timer 2 secondes** démarre
2. UI overlay : "Contenu suivant dans 2s..." + barre décompte
3. Possibilité annuler : bouton "Rester sur ce contenu" (optionnel)
4. Timer atteint 0 → passage automatique au contenu suivant
**Délai selon contexte** :
| Mode | Délai | Justification |
|------|-------|---------------|
| **Standard** | 2 secondes | Temps réaction confortable |
| **Mode Kids** | 1 seconde | Attention courte enfants |
| **Live** | 0 seconde | Enchaînement immédiat |
**Insertion publicité** :
- Pub s'insère **pendant le délai de 2s** (transition naturelle)
- Fréquence : **paramétrable admin** (défaut : 1 pub / 5 contenus)
- Message : "Publicité (15s)" puis lecture pub
- ⚠️ **Jamais d'interruption** d'un contenu en cours
**Publicité skippable** :
- Durée minimale visionnage : **paramétrable** (défaut : 5 secondes)
- Bouton "Passer" apparaît après délai
- Métriques engagement : taux skip, durée écoute moyenne
- **Like et abonnement autorisés sur pub** (engagement créateur pub)
**Si aucun contenu disponible** :
1. Message : "Aucun contenu disponible dans cette zone"
2. Proposition : "Élargir la zone de recherche ?" (bouton)
3. Si accepté → relance algo avec rayon +50km
4. Sinon → lecture en pause, attente action utilisateur
**Gestion erreurs** :
- Échec chargement contenu suivant → **retry 3× avec backoff exponentiel**
- Si 3 échecs → message "Connexion instable, basculement mode offline"
- Mode offline → lecture contenus téléchargés uniquement
**Justification** :
- **Fluidité** : enchaînement naturel sans action utilisateur
- **Contrôle** : possibilité annuler pendant délai
- **Paramétrabilité pub** : évite frustration excès publicité
- **Engagement pub** : like/abonnement autorisé = monétisation créateurs pub
---
## Récapitulatif Section 5