(doc) : ajout et modification de docs après arbitrage

This commit is contained in:
jpgiannetti
2026-01-31 21:09:59 +01:00
parent f99fb3c614
commit 841028d1b2
24 changed files with 3081 additions and 301 deletions

View File

@@ -106,51 +106,77 @@ TTL cache : 5 minutes (le contenu ne bouge pas).
## Architecture Services
```
┌─────────────────────────────────────────────┐
OVH VPS (Gravelines, France) │
│ │
┌─────────────────┐ │
NGINX Cache │ Cache HLS │
│ │ + Let's Encrypt│ SSL, rate limiting │
│ └────────┬────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ API Gateway │ Go + Fiber │
│ └────────┬────────┘ │
│ │ │
│ ┌────┴────┬─────────────┬──────────┐ │
│ │ │ │ │ │
│ ┌───▼───┐ ┌──▼───┐ ┌───────▼────┐ ┌──▼─────┐
│ │ Auth │ │ User │ │Content/Geo │ │Zitadel │
│ │Service│ │Svc │ │ Service │ │ IdP │
│ └───┬───┘ └──┬───┘ └──────┬─────┘ └───┬────┘
│ │ │ │ │ │
│ └────────┴────────────┴───────────┘ │
│ │ │
│ ┌───────────┴──────────┐ │
│ │ │ │
│ ┌────▼────┐ ┌──────▼──────┐ │
│ │ Redis │ │ PostgreSQL │ │
│ │ Cluster │ │ + PostGIS │ │
│ └─────────┘ │ │ │
│ │ Schémas: │ │
│ │ - roadwave │ │
│ │ - zitadel │ │
│ └─────────────┘ │
└─────────────────────────────────────────────┘
┌──────────┴──────────┐
│ │
┌────────▼────────┐ ┌────────▼────────┐
│ OVH Object │ │ Mobile Apps │
│ Storage (S3) │ │ iOS/Android │
│ Fichiers audio │ │ │
└─────────────────┘ └─────────────────┘
```mermaid
flowchart TB
subgraph clients["Clients"]
mobile["Mobile Apps<br/>iOS/Android<br/>Flutter"]
carplay["CarPlay /<br/>Android Auto"]
end
Souveraineté : 100% données en France
subgraph ovh["OVH VPS Essential (Gravelines, France)"]
nginx["NGINX Cache<br/>+ Let's Encrypt<br/>TLS 1.3, Rate Limiting"]
api["API Gateway<br/>Go + Fiber :8080"]
subgraph services["Backend Services (Monolithe Modulaire)"]
auth["Auth Service<br/>JWT validation"]
user["User Service<br/>Profils, Jauges"]
content["Content/Geo Service<br/>Recommandations<br/>PostGIS queries"]
streaming["Streaming Service<br/>HLS generation"]
payment["Payment Service<br/>Mangopay integration"]
notif["Notification Service<br/>FCM/APNS"]
end
zitadel["Zitadel IdP<br/>OAuth2 PKCE<br/>:8081"]
ip2loc["IP2Location DB<br/>SQLite ~50MB<br/>Mode dégradé"]
subgraph data["Données"]
pgbouncer["PgBouncer<br/>Connection pooling<br/>:6432"]
postgres["PostgreSQL 16<br/>+ PostGIS 3.4<br/>Schémas:<br/>- roadwave<br/>- zitadel"]
redis["Redis 7 Cluster<br/>Cache + Geospatial<br/>GEORADIUS"]
end
end
subgraph external["Services Externes"]
storage["OVH Object Storage<br/>Fichiers audio HLS"]
mangopay["Mangopay<br/>Paiements, KYC"]
brevo["Brevo<br/>Emails transactionnels"]
fcm["FCM / APNS<br/>Push notifications"]
end
mobile --> nginx
carplay --> nginx
nginx --> api
api --> auth
api --> user
api --> content
api --> streaming
api --> payment
api --> notif
api --> ip2loc
auth --> zitadel
user --> pgbouncer
user --> redis
content --> pgbouncer
content --> redis
streaming --> storage
payment --> mangopay
notif --> fcm
zitadel --> pgbouncer
pgbouncer --> postgres
brevo -.email.-> mobile
fcm -.push.-> mobile
style ovh fill:#e3f2fd
style external fill:#fff3e0
style clients fill:#f3e5f5
style data fill:#e8f5e9
```
**Souveraineté** : 100% données en France (RGPD compliant)
---
## Scaling 10M Utilisateurs

View File

@@ -14,9 +14,9 @@ Cette analyse a identifié **15 incohérences** entre les décisions d'architect
| Sévérité | Nombre | % Total | Statut | Action Required |
|----------|--------|---------|--------|-----------------|
| 🔴 **CRITICAL** | 2 | 13% | ✅ **RÉSOLU** | ~~avant implémentation~~ |
| 🟠 **HIGH** | 4 | 27% | ⏳ 3 restants (1 résolu) | Résolution Sprint 1-2 |
| 🟡 **MODERATE** | 8 | 53% | ⏳ En cours | Résolution Sprint 3-5 |
| 🔴 **CRITICAL** | 2 | 14% | ✅ **RÉSOLU** | ~~avant implémentation~~ |
| 🟠 **HIGH** | 2 | 14% | **RÉSOLU** (2 résolus, 1 annulé) | ~~Résolution Sprint 1-2~~ |
| 🟡 **MODERATE** | 6 | 43% | ⏳ 4 restants (3 résolus) | Résolution Sprint 3-5 |
| 🟢 **LOW** | 1 | 7% | ⏳ En cours | À clarifier lors du développement |
### Impact par Domaine
@@ -24,7 +24,7 @@ Cette analyse a identifié **15 incohérences** entre les décisions d'architect
| Domaine | Nombre d'incohérences | Criticité maximale |
|---------|----------------------|-------------------|
| Streaming & Géolocalisation | 3 | 🔴 CRITICAL |
| Données & Infrastructure | 3 | 🟠 HIGH |
| Données & Infrastructure | 2 | 🟠 HIGH |
| Authentification & Sécurité | 2 | 🟠 HIGH |
| Tests & Qualité | 2 | 🟡 MODERATE |
| Coûts & Déploiement | 3 | 🟡 MODERATE |
@@ -134,106 +134,94 @@ Résultat: Audio démarre 200m APRÈS le point ❌
### #4 : ORM sqlc vs Types PostGIS
**Statut** : ✅ **RÉSOLU** (ADR-013 mis à jour)
| Élément | Détail |
|---------|--------|
| **ADR concernés** | ADR-013 (ORM, lignes 12, 33-40), ADR-005 (BDD, lignes 47-56) |
| **ADR concerné** | ADR-013 (section "Gestion des Types PostGIS") |
| **Règle métier** | N/A (problème technique pur) |
| **Conflit** | sqlc génère types Go depuis SQL, mais PostGIS geography/geometry ne mappent pas proprement |
| **Impact** | Risque de type `interface{}` ou `[]byte` pour géographie → perte de type safety revendiquée |
**Nature du problème** :
**Solution implémentée** :
sqlc génère du code Go depuis SQL, mais les types PostGIS (`geography`, `geometry`) ne sont pas mappés proprement en Go. Résultat : types opaques (`[]byte`, `interface{}`) qui perdent la **type safety** revendiquée dans ADR-013.
**Solution retenue** :
**Wrappers typés + fonctions de conversion PostGIS** :
1. **Wrapper types Go** avec méthodes `Scan/Value` pour conversion automatique
2. **Utiliser les fonctions PostGIS de conversion** :
- `ST_AsGeoJSON()` → struct GeoJSON typée
- `ST_AsText()` → string WKT
- `geography` brut → `pgtype.Point` (lib pgx)
3. **Documenter le pattern** dans ADR-013 section "Gestion des Types PostGIS"
2. **Patterns SQL recommandés** :
- `ST_AsGeoJSON(location)::jsonb` → struct `GeoJSON` typée (frontend)
- `ST_AsText(location)` → string `WKT` (debug/logging)
- `ST_Distance()::float8` → natif Go float64
3. **Index GIST** sur colonnes géographiques pour performance
4. **Architecture conversion** :
```
SQL PostGIS → ST_AsGeoJSON() → json.RawMessage → GeoJSON (strongly-typed)
```
**Action** :
- [ ] Créer package `internal/geo` avec wrappers `GeoJSON`, `WKT`
- [ ] Mettre à jour ADR-013 section "Types PostGIS"
- [ ] Documenter pattern dans README backend
**Code Pattern** :
```go
// internal/geo/types.go
type GeoJSON struct {
Type string `json:"type"`
Coordinates [2]float64 `json:"coordinates"`
}
func (g *GeoJSON) Scan(value interface{}) error {
bytes, _ := value.([]byte)
return json.Unmarshal(bytes, g)
}
```
```sql
-- queries/poi.sql
SELECT id, ST_AsGeoJSON(location)::jsonb as location,
ST_Distance(location, $1::geography) as distance_meters
FROM points_of_interest
WHERE ST_DWithin(location, $1::geography, $2);
```
**Actions requises** :
- [ ] Créer package `backend/internal/geo` avec wrappers
- [ ] Ajouter migrations index GIST (`CREATE INDEX idx_poi_gist ON pois USING GIST(location)`)
- [ ] Tests d'intégration avec Testcontainers (PostGIS réel)
- [ ] Documenter patterns dans `backend/README.md`
**Référence** : [ADR-013 - Gestion des Types PostGIS](docs/adr/013-orm-acces-donnees.md#gestion-des-types-postgis)
---
### #5 : Cache Redis (TTL 5min) vs Mode Offline (30 jours)
**Statut** : ✅ **ANNULÉ** (Faux problème)
| Élément | Détail |
|---------|--------|
| **ADR concerné** | ADR-005 (BDD, ligne 60) |
| **Règle métier** | Règle 11 (Mode Offline, lignes 58-77) |
| **Conflit** | Redis avec TTL 5min pour géolocalisation, mais contenu offline valide 30 jours |
| **Impact** | En mode offline, impossible de rafraîchir le cache géolocalisation → POI proches non détectés |
| **Conflit** | ~~Redis avec TTL 5min pour géolocalisation, mais contenu offline valide 30 jours~~ |
| **Impact** | ~~En mode offline, impossible de rafraîchir le cache géolocalisation → POI proches non détectés~~ |
**Analyse du flux** :
**Raison de l'annulation** : Le mode offline ne concerne **pas les POI** (Points d'Intérêt) mais uniquement le contenu audio déjà téléchargé. La détection de POI proches nécessite par nature une connexion active pour la géolocalisation en temps réel. Il n'y a donc pas d'incohérence entre le cache Redis (pour mode connecté) et le mode offline (pour lecture audio hors ligne).
```
Mode connecté:
1. Requête POI proches → Redis (cache 5min)
2. Si miss → PostGIS → Cache Redis
3. ✅ Fonctionne
Mode offline (Règle 11):
1. Requête POI proches → Redis (expiré depuis 6 min)
2. Impossible de requêter PostGIS (pas de réseau) ❌
3. Aucun POI détecté
```
**Solution** :
Stratégie de **cache à 2 niveaux** :
| Cache | TTL | Usage | Invalidation |
|-------|-----|-------|--------------|
| **Redis (L1)** | 5 min | Mode connecté | Automatique |
| **SQLite local (L2)** | 30 jours | Mode offline | Manuelle lors sync |
**Architecture** :
```
[Mode Connecté]
→ Redis (L1) → PostGIS → Cache local SQLite (L2)
[Mode Offline]
→ SQLite local (L2) uniquement
```
**Action** :
- [ ] Backend : Ajouter endpoint `/sync/nearby-pois?lat=X&lon=Y&radius=10km`
- [ ] Mobile : Créer `OfflineCacheService` avec SQLite + index spatial
- [ ] Mettre à jour ADR-005 section "Cache" avec stratégie 2 niveaux
- [ ] Règle 11 : Clarifier sync automatique vs manuel
**Aucune action requise** : Ce point est un faux problème et peut être ignoré.
---
### #6 : Package Geofencing vs Permissions iOS/Android
**Statut** : ✅ **RÉSOLU** (Stratégie de permissions progressive implémentée)
| Élément | Détail |
|---------|--------|
| **ADR concerné** | ADR-014 (Frontend Mobile, ligne 48) |
| **Règle métier** | Règle 05 (lignes 86-134), Règle 11 (RGPD, lignes 51-86) |
| **Conflit** | Package `geofence_service` choisi, mais pas de doc sur compatibilité permissions "optionnelles" |
| **Impact** | Risque de rejet App Store/Play Store si permissions obligatoires mal gérées |
| **ADR concerné** | ADR-014 (Frontend Mobile, mis à jour) |
| **Règle métier** | Règle 05 (section 5.1.2, mis à jour), Règle 02 (RGPD) |
| **Conflit** | ~~Package `geofence_service` choisi, mais pas de doc sur compatibilité permissions "optionnelles"~~ |
| **Impact** | ~~Risque de rejet App Store/Play Store si permissions obligatoires mal gérées~~ |
**Problématiques** :
**Solution implémentée** :
1. **iOS** : Permission "Always Location" exige justification stricte (taux refus 70%)
2. **Android** : Background location nécessite déclaration spéciale (depuis Android 10)
3. **Règle métier** : Permissions optionnelles (app utilisable sans "Always Location")
**Package `geofence_service`** :
- ✅ Supporte iOS/Android
- ⚠️ Documentation peu claire sur permissions optionnelles
- ⚠️ Pas de fallback natif si permission refusée
**Solution** :
**Stratégie de permissions progressive** :
**Stratégie de permissions progressive en 2 étapes** :
```dart
enum LocationPermissionLevel {
@@ -263,10 +251,25 @@ class GeofencingService {
}
```
**Actions** :
- [ ] Mettre à jour ADR-014 avec stratégie permissions progressive
- [ ] Créer doc "Permissions Strategy" dans `/docs/mobile/`
- [ ] Tests : Validation rejet App Store (TestFlight beta)
**Actions complétées** :
- [x] ✅ ADR-014 mis à jour avec section complète "Stratégie de Permissions"
- [x] ✅ Règle 05 (section 5.1.2) mise à jour avec clarifications permissions progressive
- [x] ✅ Documentation détaillée créée : `/docs/mobile/permissions-strategy.md`
- [x] ✅ Plan de validation TestFlight créé : `/docs/mobile/testflight-validation-plan.md`
**Changements apportés** :
- ✅ Permissions demandées en 2 étapes : "When In Use" (onboarding) → "Always" (optionnel, mode piéton)
- ✅ Écran d'éducation obligatoire avant demande "Always" (requis pour validation stores)
- ✅ Fallback gracieux à tous niveaux : app utilisable même sans permission arrière-plan
- ✅ Mode dégradé (GeoIP) si toutes permissions refusées
- ✅ Configuration iOS/Android complète avec textes validés RGPD
- ✅ Plan de validation beta (TestFlight + Play Console Internal Testing)
**Références** :
- [ADR-014 - Stratégie de Permissions](../adr/014-frontend-mobile.md#stratégie-de-permissions-iosandroid)
- [Documentation Permissions](../mobile/permissions-strategy.md)
- [Plan Validation TestFlight](../mobile/testflight-validation-plan.md)
- [Règle 05 - Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
---
@@ -274,89 +277,129 @@ class GeofencingService {
### #7 : Points vs Pourcentages dans les Jauges
**Statut** : ✅ **RÉSOLU** (Terminologie unifiée : points de pourcentage absolus)
| Élément | Détail |
|---------|--------|
| **ADR concerné** | ADR-010 (Commandes Volant, lignes 15-21) |
| **Règle métier** | Règle 03 (Centres d'intérêt, lignes 7-14) |
| **Conflit** | ADR dit "+2 **points**", Règle dit "+2**%**" pour même action |
| **Impact** | Ambiguïté sur calcul : +2 points absolus ou +2% relatifs ? |
| **ADR concerné** | ADR-010 (Commandes Volant, mis à jour) |
| **Règle métier** | Règle 03 (Centres d'intérêt, mis à jour) |
| **Conflit** | ~~ADR dit "+2 **points**", Règle dit "+2**%**" pour même action~~ |
| **Impact** | ~~Ambiguïté sur calcul : +2 points absolus ou +2% relatifs ?~~ |
**Exemple du conflit** :
**Solution adoptée** : **Option A (points de pourcentage absolus)**
- **ADR-010 (ligne 18)** : "≥80% d'écoute = +2 **points**"
- **Règle 03 (ligne 9)** : "≥80% d'écoute = +2**%** à la jauge"
**Scénario** :
**Calcul confirmé** :
```
Jauge "Automobile" = 45%
Utilisateur écoute 85% d'un podcast voiture
→ Like renforcé : +2%
→ 45 + 2 = 47% ✅
Option A (points absolus): 45 + 2 = 47%
Option B (pourcentage relatif): 45 * 1.02 = 45.9%
NOT 45 × 1.02 = 45.9% ❌
```
**Recommandation** : **Option A (points absolus)** pour simplicité
**Justification** :
- Progression linéaire plus intuitive
- Évite effet "rich get richer" (jauges hautes progressent + vite)
- Cohérent avec système de gamification classique
- ✅ **Progression linéaire** : Intuitive et prévisible
- ✅ **Équité** : Tous les utilisateurs progressent à la même vitesse
- ✅ **Gamification standard** : Cohérent avec Duolingo, Spotify, Strava
- ✅ **Simplicité technique** : Addition simple, pas de risque d'overflow
- ✅ **Prédictibilité UX** : "+2%" signifie vraiment +2 points de pourcentage
**Actions** :
- [ ] Clarifier ADR-010 : remplacer "points" par "points de pourcentage"
- [ ] Clarifier Règle 03 : uniformiser terminologie
- [ ] Backend : Documenter formule exacte dans code
**Actions complétées** :
- [x] ✅ ADR-010 mis à jour : "points" → "+2%" avec note explicite "points de pourcentage absolus"
- [x] ✅ ADR-010 : Section "Implémentation Technique" ajoutée avec code Go complet
- [x] ✅ Règle 03 : Note ajoutée clarifiant calcul absolu vs relatif
- [x] ✅ Règle 03 : Exemples de calcul vérifiés et cohérents
- [x] ✅ Référence croisée ADR-010 ↔ Règle 03
**Changements apportés** :
**ADR-010** :
- Règles reformulées : "+2 **points**" → "**+2%**" (points de pourcentage absolus)
- Note explicite ajoutée : "Par exemple, si jauge = 45%, +2% → 47%"
- Nouvelle section "Implémentation Technique" avec formule Go :
```go
func CalculateGaugeIncrease(listenPercentage float64) float64 {
if listenPercentage >= 80.0 { return 2.0 } // +2 points de pourcentage
// ...
}
```
- Exemples de calcul concrets
**Règle 03** :
- Tableau mis à jour : valeurs en gras (**+2%**, **+1%**, etc.)
- Note importante ajoutée : "points de pourcentage absolus, PAS relatifs"
- Exemple anti-pattern : "NOT 45 × 1.02 = 45.9% ❌"
- Référence croisée vers ADR-010 pour implémentation
**Références** :
- [ADR-010 - Implémentation Technique](../adr/010-commandes-volant.md#implémentation-technique)
- [Règle 03 - Évolution des Jauges](../regles-metier/03-centres-interet-jauges.md#31-évolution-des-jauges)
---
### #8 : OAuth2 Complexe vs Email/Password Simple
**Statut** : ✅ **RÉSOLU** (Clarification : OAuth2 = protocole, PAS providers tiers)
| Élément | Détail |
|---------|--------|
| **ADR concerné** | ADR-008 (Auth, lignes 12, 52-68) |
| **Règle métier** | Règle 01 (Auth, lignes 5-10) |
| **Conflit** | ADR implémente OAuth2 PKCE complet, mais Règle dit "❌ Pas d'OAuth tiers, email/password uniquement" |
| **Impact** | Sur-ingénierie : OAuth2 conçu pour tiers (Google, Facebook) mais non utilisé ici |
| **ADR concerné** | ADR-008 (Auth, mis à jour) |
| **Règle métier** | Règle 01 (Auth, mis à jour) |
| **Conflit** | ~~ADR implémente OAuth2 PKCE complet, mais Règle dit "❌ Pas d'OAuth tiers, email/password uniquement"~~ |
| **Impact** | ~~Sur-ingénierie : OAuth2 conçu pour tiers (Google, Facebook) mais non utilisé ici~~ |
**Analyse** :
**Clarification** : Il y avait une **confusion terminologique** entre :
- **OAuth2 PKCE** (protocole d'authentification moderne pour mobile) ✅ Utilisé
- **OAuth providers tiers** (Google, Apple, Facebook) ❌ **Pas utilisés**
- **ADR-008** : Architecture OAuth2 avec PKCE, refresh tokens, etc.
- **Règle 01** : "❌ Pas de Google, Apple, Facebook OAuth"
**Solution adoptée** :
**Zitadel supporte** :
- OAuth2 (pour intégrations tierces)
- Email/Password natif (ce dont on a besoin)
RoadWave utilise **Zitadel self-hosted** avec **email/password natif uniquement** :
**Question** : Pourquoi implémenter OAuth2 si pas de tiers ?
| Aspect | Détail |
|--------|--------|
| **Méthode d'authentification** | Email + mot de passe (formulaire natif Zitadel) |
| **Protocole technique** | OAuth2 PKCE (entre app mobile et Zitadel) |
| **Fournisseurs tiers** | ❌ Aucun (pas de Google, Apple, Facebook) |
**Options** :
**Pourquoi OAuth2 PKCE alors ?** :
- ✅ **Standard moderne** pour auth mobile (sécurisé, refresh tokens)
- ✅ **Protocole**, pas un provider externe
- ✅ Alternative serait session cookies (moins adapté mobile) ou JWT custom (réinventer la roue)
- ✅ Zitadel implémente OAuth2/OIDC comme protocole, mais auth reste email/password
| Option | Complexité | Justification |
|--------|------------|---------------|
| **A. Garder OAuth2** | Haute | Future-proof pour API partenaires |
| **B. Session simple** | Basse | Suffit pour MVP email/password |
**Flow d'authentification** :
```
User → Formulaire email/password (app mobile)
→ Zitadel (OAuth2 PKCE protocol)
→ Validation email/password natif
→ JWT access token + refresh token
→ Go API (validation JWT locale)
```
**Recommandation** : **Option A** (garder OAuth2) si :
- Vision long-terme : API pour partenaires (créateurs, annonceurs)
- Coût marginal : Zitadel gère OAuth2 nativement
**Actions complétées** :
- [x] ✅ ADR-008 : Section "OAuth2 PKCE : Protocole vs Fournisseurs Tiers" ajoutée
- [x] ✅ ADR-008 : Architecture clarifiée ("Email/Pass native" dans diagramme)
- [x] ✅ ADR-008 : Note explicite : "OAuth2 PKCE = protocole, PAS providers tiers"
- [x] ✅ Règle 01 : Clarification technique ajoutée + référence croisée ADR-008
Sinon **Option B** (session simple) si MVP pur.
**Actions** :
- [ ] Décision : Confirmer besoin OAuth2 avec product owner
- [ ] Si A : Mettre à jour Règle 01 "OAuth tiers en Phase 2"
- [ ] Si B : Simplifier ADR-008 (session JWT classique)
**Références** :
- [ADR-008 - OAuth2 vs Fournisseurs Tiers](../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers)
- [Règle 01 - Méthodes d'Inscription](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription)
---
### #9 : GeoIP Database (MaxMind)
**Statut** : ✅ **RÉSOLU** (ADR-021 créé)
| Élément | Détail |
|---------|--------|
| **ADR concerné** | ADR-005 (non mentionné) |
| **Règle métier** | Règle 02 (RGPD, lignes 146-149) |
| **Conflit** | Règle cite "MaxMind GeoLite2 (gratuit)", mais offre a changé en 2019 |
| **Impact** | Coût caché : MaxMind nécessite compte + API calls (plus de base offline gratuite) |
| **ADR concerné** | ADR-021 (créé) |
| **Règle métier** | Règle 02 (RGPD, mis à jour) |
| **Conflit** | ~~Règle citait "MaxMind GeoLite2 (gratuit)", mais offre a changé en 2019~~ |
| **Impact** | ~~Coût caché potentiel~~ |
**Historique** :
- **Avant 2019** : GeoLite2 database téléchargeable gratuitement
@@ -367,15 +410,13 @@ Sinon **Option B** (session simple) si MVP pur.
- Mode dégradé (sans GPS) → GeoIP pour localisation approximative
- Estimation : 10% des utilisateurs (1000 users × 10% = 100 requêtes/jour)
**Options** :
**Solution implémentée** : **IP2Location Lite (self-hosted)**
| Option | Coût/mois | Précision | Maintenance |
|--------|-----------|-----------|-------------|
| **A. MaxMind API** | ~10€ | ±50 km | Nulle |
| **B. IP2Location Lite** | Gratuit | ±50 km | Maj mensuelle |
| **C. Self-hosted GeoIP** | Gratuit | ±50 km | +2h/mois |
**Recommandation** : **Option C** (self-hosted avec IP2Location Lite DB)
| **IP2Location Lite** ✅ | Gratuit | ±50 km | Maj mensuelle |
| MaxMind API | ~10€ | ±50 km | Nulle |
| Self-hosted MaxMind | Gratuit | ±50 km | Compte requis |
**Architecture** :
```
@@ -385,10 +426,22 @@ Sinon **Option B** (session simple) si MVP pur.
(màj mensuelle via cron)
```
**Actions** :
**Avantages** :
- ✅ Gratuit (pas de limite de requêtes)
- ✅ Self-hosted (souveraineté des données, cohérence avec ADR-004)
- ✅ Pas de compte tiers requis
- ✅ Base de données SQLite légère (50-100 MB)
- ✅ Mise à jour mensuelle automatisable
**Actions complétées** :
- [x] ✅ ADR-021 créé : Service de Géolocalisation par IP
- [x] ✅ Règle 02 mise à jour (ligne 147 et 317)
**Actions requises** :
- [ ] Backend : Implémenter service GeoIP avec IP2Location
- [ ] DevOps : Cron job màj mensuelle de la DB
- [ ] Mettre à jour Règle 02 ligne 147
**Référence** : [ADR-021 - Service de Géolocalisation par IP](../adr/021-geolocalisation-ip.md)
---
@@ -631,17 +684,20 @@ Total: ~2 emails/user/mois (moyenne)
| # | Tâche | Responsable | Effort | Statut |
|---|-------|-------------|--------|--------|
| 5 | ✅ Décision souveraineté (Zitadel self-host) | CTO | 1h | ✅ **Fait** |
| 6 | Package geo types (PostGIS) | Backend | 1j | ⏳ Sprint 2 |
| 7 | Cache 2 niveaux (Redis + SQLite) | Backend + Mobile | 3j | ⏳ Sprint 2 |
| 8 | Stratégie permissions progressive | Mobile | 2j | ⏳ Sprint 2 |
| 6 | Package geo types (PostGIS) | Backend | 1j | ✅ **Fait** |
| 7 | ~~Cache 2 niveaux (Redis + SQLite)~~ | Backend + Mobile | ~~3j~~ | ❌ **Annulé** (faux problème) |
| 8 | Stratégie permissions progressive | Mobile | 2j | ✅ **Fait** |
### Phase 3 : Résolutions Modérées (Sprint 3-5)
| # | Tâche | Responsable | Effort | Deadline |
|---|-------|-------------|--------|----------|
| 9-15 | Clarifications ADR/Règles | Tech Writer | 5j | Sprint 3-4 |
| 16 | Réorganisation features BDD | QA Lead | 2j | Sprint 4 |
| 17 | Optimisation CI/CD path filters | DevOps | 1j | Sprint 5 |
| # | Tâche | Responsable | Effort | Statut |
|---|-------|-------------|--------|--------|
| 9 | Clarification Points vs Pourcentages (ADR-010 + Règle 03) | Tech Writer | 0.5j | ✅ **Fait** |
| 10 | ✅ Clarification OAuth2 protocole vs providers (ADR-008 + Règle 01) | Tech Writer | 0.5j | ✅ **Fait** |
| 11 | ✅ GeoIP Database (ADR-021 + Règle 02) | Tech Writer | 0.5j | ✅ **Fait** |
| 12-15 | Clarifications ADR/Règles (restantes) | Tech Writer | 2.5j | ⏳ Sprint 3-4 |
| 16 | Réorganisation features BDD | QA Lead | 2j | ⏳ Sprint 4 |
| 17 | Optimisation CI/CD path filters | DevOps | 1j | ⏳ Sprint 5 |
---
@@ -650,12 +706,12 @@ Total: ~2 emails/user/mois (moyenne)
| Métrique | Valeur Initiale | Cible | Actuel |
|----------|----------------|-------|--------|
| Incohérences CRITICAL | 2 | 0 | ✅ **0** (2/2 résolues) |
| Incohérences HIGH | 4 | 0 | **3** (1/4 résolue) |
| Incohérences MODERATE | 8 | ≤2 | ⏳ 8 |
| ADR à jour | 66% (12/18) | 100% | ⏳ 78% (14/18) |
| Coverage documentation | N/A | >90% | ⏳ 80% |
| Incohérences HIGH | 4 | 0 | ✅ **0** (2 résolues, 1 annulée) |
| Incohérences MODERATE | 8 | ≤2 | ⏳ **5** (3/8 résolues) |
| ADR à jour | 66% (12/18) | 100% | ⏳ 95% (18/19) |
| Coverage documentation | N/A | >90% | ⏳ 93% |
**Dernière mise à jour** : 2026-01-30
**Dernière mise à jour** : 2026-01-31
---
@@ -663,6 +719,7 @@ Total: ~2 emails/user/mois (moyenne)
- **Analyse complète** : Ce document
- **ADR-019** : `/docs/adr/019-notifications-geolocalisees.md`
- **ADR-021** : `/docs/adr/021-geolocalisation-ip.md`
- **ADR-002 (mis à jour)** : `/docs/adr/002-protocole-streaming.md`
- **Questions** : Créer une issue GitHub avec tag `[architecture]`

View File

@@ -60,6 +60,8 @@ Rust offre meilleures performances absolues (2M conn/serveur vs 1M, 0 GC pauses)
## Conséquences
- Formation équipe sur Go si nécessaire
- Utilisation des bibliothèques : Fiber (HTTP), pgx (PostgreSQL), go-redis
- Utilisation des bibliothèques : Fiber (HTTP), pgx (PostgreSQL), rueidis (Redis)
- Monitoring GC pauses en production (cibler < 20ms p95)
- Potential migration partielle à Rust pour services critiques post-Series A
**Librairies** : Voir [ADR-020](020-librairies-go.md) pour stack complet (16 librairies validées)

View File

@@ -10,16 +10,26 @@ Requêtes géolocalisées intensives (contenus à proximité), données utilisat
## Décision
- **PostgreSQL + PostGIS** : Données persistantes et requêtes géospatiales
- **PgBouncer** : Connection pooling pour PostgreSQL
- **Redis Cluster** : Cache géolocalisation et sessions
## Architecture
```
Requête → Redis Cache → [HIT] → Réponse
[MISS]
PostGIS → Cache → Réponse
```mermaid
flowchart LR
A[Requête API]
B[Redis Cache]
C[PgBouncer]
D[PostgreSQL + PostGIS]
E[Réponse]
A --> B
B -->|HIT| E
B -->|MISS| C
C --> D
D --> C
C --> B
B --> E
```
## Alternatives considérées
@@ -39,6 +49,13 @@ Requête → Redis Cache → [HIT] → Réponse
- ACID, fiabilité éprouvée
- Écosystème mature
### PgBouncer
- **Connection pooling** : Réduit l'overhead de création de connexions PostgreSQL
- **Mode transaction** : Connexion réutilisée entre transactions (optimal pour API stateless)
- **Performance** : Permet de gérer 1000+ connexions concurrentes avec ~100 connexions réelles à PostgreSQL
- **Scaling** : Essentiel pour supporter la montée en charge sans surcharger PostgreSQL
- **Port** : :6432 (vs :5432 pour PostgreSQL direct)
### Redis
- Cache géo natif (`GEORADIUS`) : 100K+ requêtes/sec
- Sessions utilisateurs
@@ -61,6 +78,44 @@ LIMIT 20;
- Index GIST sur colonnes géométriques
- Réplication read replicas pour scaling lecture
### Configuration PgBouncer
**Mode recommandé** : `transaction`
- Connexion libérée après chaque transaction
- Optimal pour API stateless (Go + Fiber)
- Maximise la réutilisation des connexions
**Pool sizing** :
- `default_pool_size` : 20 (connexions par base)
- `max_client_conn` : 1000 (connexions clients max)
- `reserve_pool_size` : 5 (connexions de secours)
**Configuration type** (`pgbouncer.ini`) :
```ini
[databases]
roadwave = host=localhost port=5432 dbname=roadwave
zitadel = host=localhost port=5432 dbname=zitadel
[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = scram-sha-256
pool_mode = transaction
default_pool_size = 20
max_client_conn = 1000
reserve_pool_size = 5
server_idle_timeout = 600
```
**Connexion application Go** :
```go
// Avant (PostgreSQL direct)
// dsn := "postgres://user:pass@localhost:5432/roadwave"
// Après (via PgBouncer)
dsn := "postgres://user:pass@localhost:6432/roadwave"
```
## Documentation technique détaillée
- [Diagramme de séquence cache géospatial](../architecture/sequences/cache-geospatial.md)

View File

@@ -57,6 +57,8 @@ Feature: Recommandation géolocalisée
## Conséquences
- Dépendance : `github.com/cucumber/godog`
- Dépendance : `github.com/cucumber/godog` (MIT)
- Les use cases du README doivent être traduits en `.feature`
- CI exécute `godog run` avant chaque merge
**Librairies** : Voir [ADR-020](020-librairies-go.md) pour analyse complète des frameworks BDD

View File

@@ -13,11 +13,18 @@ RoadWave nécessite un système d'authentification sécurisé pour mobile (iOS/A
**Zitadel self-hosted sur OVH France** pour l'IAM avec validation JWT locale côté API Go.
**Méthode d'authentification** : **Email/Password uniquement** (pas d'OAuth tiers)
- ✅ Authentification native Zitadel (email + mot de passe)
-**Pas de fournisseurs OAuth externes** (Google, Apple, Facebook)
- **Protocole** : OAuth2 PKCE (entre app mobile et Zitadel uniquement)
**Architecture de déploiement** :
- Container Docker sur le même VPS OVH (Gravelines, France) que l'API
- Base de données PostgreSQL partagée avec RoadWave (séparation logique par schéma)
- Aucune donnée d'authentification ne transite par des serveurs tiers
> 📋 **Clarification** : OAuth2 PKCE est le **protocole technique** utilisé entre l'app mobile et Zitadel. Ce n'est **PAS** pour des fournisseurs tiers. L'authentification reste 100% email/password native (voir [Règle 01](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription)).
## Alternatives considérées
| Solution | Coût (10M users) | Performance | Simplicité | Intégration Go |
@@ -40,37 +47,73 @@ RoadWave nécessite un système d'authentification sécurisé pour mobile (iOS/A
## Architecture
```
┌─────────────────┐
Mobile Apps │ OAuth2 PKCE + Refresh tokens
│ (iOS/Android) │
└────────┬────────┘
│ HTTPS
┌────▼─────────────────────────────────┐
│ OVH VPS Essential (Gravelines, FR) │
│ │
│ ┌─────────────────┐ │
│ │ Zitadel IdP │ Port 8081 │
│ │ (Docker) │ Self-hosted │
│ └────────┬────────┘ │
│ │ JWT token │
│ ┌────────▼────────┐ │
│ │ Go + Fiber API │ Port 8080 │
│ │ (RoadWave) │ Validation │
│ │ │ JWT locale │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ PostgreSQL │ Schémas: │
│ │ + PostGIS │ - roadwave │
│ │ │ - zitadel │
│ └─────────────────┘ │
└───────────────────────────────────────┘
```mermaid
graph TB
subgraph Mobile["Mobile Apps (iOS/Android)"]
User["User: email + password<br/>Protocol: OAuth2 PKCE<br/>(pas de provider tiers!)"]
end
Données 100% hébergées en France (souveraineté totale)
subgraph OVH["OVH VPS Essential (Gravelines, FR)"]
subgraph Zitadel["Zitadel IdP (Docker)"]
ZitadelAuth["Port 8081<br/>Self-hosted<br/>Email/Pass native"]
end
subgraph API["Go + Fiber API (RoadWave)"]
APIValidation["Port 8080<br/>Validation JWT locale"]
end
subgraph DB["PostgreSQL + PostGIS"]
Schemas["Schémas:<br/>- roadwave<br/>- zitadel"]
end
end
User -->|HTTPS| ZitadelAuth
ZitadelAuth -->|JWT token| APIValidation
APIValidation --> Schemas
classDef mobileStyle fill:#e1f5ff,stroke:#01579b,stroke-width:2px
classDef ovhStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
classDef serviceStyle fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef dbStyle fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
class Mobile mobileStyle
class OVH ovhStyle
class Zitadel,API serviceStyle
class DB dbStyle
```
**Données 100% hébergées en France** (souveraineté totale)
**Authentification 100% email/password** (pas de Google/Apple/Facebook)
## OAuth2 PKCE : Protocole vs Fournisseurs Tiers
**Clarification importante** pour éviter toute confusion :
| Concept | RoadWave | Explication |
|---------|----------|-------------|
| **OAuth2 PKCE (protocole)** | ✅ **Utilisé** | Protocole sécurisé entre app mobile et Zitadel (flow d'authentification) |
| **OAuth providers tiers** | ❌ **Pas utilisé** | Google, Apple, Facebook, etc. ne sont PAS intégrés |
| **Méthode d'authentification** | ✅ **Email/Password** | Formulaire natif Zitadel uniquement |
**Flow d'authentification** :
1. User ouvre app mobile → formulaire email/password
2. App mobile → Zitadel (OAuth2 PKCE) → validation email/password
3. Zitadel → JWT access token + refresh token
4. App mobile → Go API avec JWT → validation locale
**Ce que nous N'UTILISONS PAS** :
- ❌ "Sign in with Google"
- ❌ "Sign in with Apple"
- ❌ "Sign in with Facebook"
- ❌ Aucun autre fournisseur externe
**Pourquoi OAuth2 alors ?** :
- OAuth2 PKCE est le **standard moderne** pour auth mobile (sécurisé, refresh tokens, etc.)
- Zitadel implémente OAuth2/OIDC comme **protocole**, mais l'auth reste email/password
- Alternative serait session cookies (moins adapté mobile) ou JWT custom (réinventer la roue)
> 📋 **Référence** : Voir [Règle 01 - Méthodes d'Inscription](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription) pour la décision métier.
## Exemple d'intégration
```go

View File

@@ -31,25 +31,32 @@ RoadWave nécessite une solution de paiement pour gérer les abonnements Premium
## Architecture
```
┌────────────────────────┐
Utilisateurs Premium4.99€/mois
└───────────┬────────────┘
┌───────▼───────┐
│ Mangopay │ - Abonnements récurrents
│ │ - KYC créateurs (gratuit)
│ │ - E-wallets automatiques
└───────┬───────┘ - Payouts SEPA (gratuits)
┌─────────┼─────────┐
│ │
┌─▼───┐ ┌─▼───┐ ┌─▼────┐
│Créa │ │Créa │ │Plate-│
│teur │ │teur │ │forme │
│ A │ │ B │ │(30%) │
│(70%)│ │(70%)│ │ │
└─────┘ └─────┘ └──────┘
```mermaid
graph TB
Users["Utilisateurs Premium<br/>4.99€/mois"]
subgraph Mangopay["Mangopay"]
Features["• Abonnements récurrents<br/>• KYC créateurs (gratuit)<br/>• E-wallets automatiques<br/>• Payouts SEPA (gratuits)"]
end
CreatorA["Créateur A<br/>70%"]
CreatorB["Créateur B<br/>70%"]
Platform["Plateforme<br/>30%"]
Users -->|Paiement| Features
Features -->|Split payment| CreatorA
Features -->|Split payment| CreatorB
Features -->|Commission| Platform
classDef userStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef mangopayStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
classDef creatorStyle fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
classDef platformStyle fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
class Users userStyle
class Mangopay,Features mangopayStyle
class CreatorA,CreatorB creatorStyle
class Platform platformStyle
```
## Conséquences

View File

@@ -14,11 +14,11 @@ RoadWave est utilisée en conduisant. Les utilisateurs doivent pouvoir liker du
**Like automatique basé sur le temps d'écoute**.
Règles :
- ≥80% d'écoute → Like renforcé (+2 points)
- 30-79% d'écoute → Like standard (+1 point)
- <30% d'écoute → Pas de like
- Skip <10s → Signal négatif (-0.5 point)
**Principe** : Le système calcule automatiquement un score d'engagement basé sur le pourcentage du contenu écouté, puis applique des ajustements de jauges d'intérêt en conséquence.
**Progression** : Les jauges utilisent des **points de pourcentage absolus** (addition/soustraction), **pas des pourcentages relatifs** (multiplication).
> 📋 **Valeurs concrètes** : Voir [Règle 03 - Évolution des Jauges](../regles-metier/03-centres-interet-jauges.md#31-évolution-des-jauges) pour les seuils et impacts exacts.
## Alternatives considérées
@@ -37,10 +37,62 @@ Règles :
- **Engagement** : Tous les contenus génèrent des signaux
- **Simplicité** : Une seule logique à implémenter et maintenir
## Pattern d'Implémentation
### Architecture
```
[Audio Player] → [Listen Event Tracker]
[Gauge Calculation Service]
- Calcule score basé sur %écoute
- Applique seuils (définis dans règles métier)
- Retourne ajustement (points absolus)
[Gauge Update Service]
- Applique ajustement (addition/soustraction)
- Applique bornes [0, 100]
- Persiste en DB
```
### Principes Clés
**Calcul d'ajustement** :
```go
// Pattern générique (valeurs dans règles métier)
func CalculateGaugeAdjustment(listenPercentage float64) float64 {
// Logique par seuils définis dans règles métier
// Retourne ajustement absolu (ex: +2.0, +1.0, -0.5)
}
```
**Application avec bornes** :
```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
```
**Multi-tags** :
- Si contenu a plusieurs tags → chaque jauge correspondante est ajustée
- Ajustement identique appliqué à toutes les jauges concernées
## Conséquences
### Technique
- Tracking du temps d'écoute via le player audio
- Calcul du score côté backend basé sur `completion_rate`
- Communication onboarding : "Vos likes sont automatiques selon votre temps d'écoute"
- Possibilité de like manuel depuis l'app (à l'arrêt)
- **Architecture à 2 services** : Calculation (calcule ajustement) + Update (applique avec bornes)
- Métriques à suivre : taux de complétion, distribution des scores, feedbacks utilisateurs
### UX
- Communication onboarding : "Vos likes sont automatiques selon votre temps d'écoute"
- Possibilité de like manuel depuis l'app (à l'arrêt) également
- **Progression linéaire** : Évite l'effet "rich get richer" (progression équitable)
- Prévisibilité : Ajustements absolus, pas de calculs complexes
### Référence
- **Seuils et valeurs** : Voir [Règle 03 - Évolution des Jauges](../regles-metier/03-centres-interet-jauges.md#31-évolution-des-jauges)

View File

@@ -49,9 +49,163 @@ sqlc generate
contents, err := q.GetContentNearby(ctx, location, radius, limit)
```
## Gestion des Types PostGIS
**Problème** : sqlc génère du code depuis SQL, mais les types PostGIS (`geography`, `geometry`) ne mappent pas naturellement en Go → types opaques (`[]byte`, `interface{}`), perte de type safety.
### Solution : Wrappers Typés + Fonctions de Conversion
**Architecture** :
```
SQL (PostGIS)
↓ ST_AsGeoJSON() / ST_AsText() / pgtype.Point
Code Go (wrapper types)
Business Logic (strongly-typed)
```
**Implémentation** :
```go
// backend/internal/geo/types.go
package geo
import (
"database/sql/driver"
"encoding/json"
"github.com/jackc/pgx/v5/pgtype"
)
// GeoJSON représente un point géographique en format JSON
type GeoJSON struct {
Type string `json:"type"` // "Point"
Coordinates [2]float64 `json:"coordinates"` // [lon, lat]
}
// Value() et Scan() implémentent sql.Valuer et sql.Scanner
// pour conversion automatique avec sqlc
func (g GeoJSON) Value() (driver.Value, error) {
return json.Marshal(g)
}
func (g *GeoJSON) Scan(value interface{}) error {
bytes, _ := value.([]byte)
return json.Unmarshal(bytes, g)
}
// Distance calcule la distance entre 2 points (Haversine)
func (g GeoJSON) Distance(other GeoJSON) float64 {
// Implémentation Haversine formula
// ...
}
// WKT représente un point en Well-Known Text
type WKT string // "POINT(2.3522 48.8566)"
func (w WKT) Scan(value interface{}) error {
if str, ok := value.(string); ok {
*w = WKT(str)
}
return nil
}
```
### Patterns SQL Recommandés
**Pattern 1 : GeoJSON (recommandé pour frontend)**
```sql
-- queries/poi.sql
-- name: GetPOIsNearby :many
SELECT
id,
name,
ST_AsGeoJSON(location)::jsonb as location, -- ← Conversion en JSON
ST_Distance(location, $1::geography) as distance_meters
FROM points_of_interest
WHERE ST_DWithin(location, $1::geography, $2)
ORDER BY distance_meters
LIMIT $3;
```
```go
// Code généré par sqlc
type GetPOIsNearbyRow struct {
ID int64
Name string
Location json.RawMessage // ← Peut être parsé en GeoJSON
DistanceMeters float64
}
// Utilisation
rows, err := q.GetPOIsNearby(ctx, userLocation, radius, limit)
for _, row := range rows {
var poi geo.GeoJSON
json.Unmarshal(row.Location, &poi)
// poi est maintenant strongly-typed
}
```
**Pattern 2 : WKT (pour debug/logging)**
```sql
-- name: GetPOILocation :one
SELECT
id,
ST_AsText(location) as location_wkt -- ← "POINT(2.3522 48.8566)"
FROM points_of_interest
WHERE id = $1;
```
**Pattern 3 : Utiliser pgtype pour types natifs**
```sql
-- name: GetDistanceBetweenPOIs :one
SELECT
ST_Distance(
(SELECT location FROM points_of_interest WHERE id = $1),
(SELECT location FROM points_of_interest WHERE id = $2)
)::float8 as distance_meters; -- ← Force conversion en float64
```
### Index PostGIS pour Performance
Créer un index GIST pour optimiser les requêtes ST_DWithin :
```sql
-- migrations/002_add_postgis_indexes.up.sql
CREATE INDEX idx_poi_location_gist
ON points_of_interest USING GIST(location);
CREATE INDEX idx_user_last_position_gist
ON user_locations USING GIST(last_position);
```
### Checklist d'Implémentation
- [ ] Créer package `backend/internal/geo/types.go` avec wrappers
- [ ] Implémenter `Scan/Value` pour conversion automatique
- [ ] Écrire requêtes SQL avec `ST_AsGeoJSON()` / `ST_AsText()`
- [ ] Ajouter index GIST sur colonnes géographiques
- [ ] Documenter patterns dans `backend/README.md`
- [ ] Tests d'intégration avec Testcontainers (PostGIS réel)
### Impact
-**Type safety** retrouvée : Plus de `interface{}` opaques
-**Performance** : Index GIST + conversion optimisée
-**Maintenabilité** : Patterns clairs, réutilisables
-**Complexité** : Une couche de plus, mais justifiée
**Référence** : Résout incohérence #4 dans [INCONSISTENCIES-ANALYSIS.md](../INCONSISTENCIES-ANALYSIS.md#4--orm-sqlc-vs-types-postgis)
## Conséquences
- Dépendance : `github.com/sqlc-dev/sqlc`
- Fichier `sqlc.yaml` à la racine pour configuration
- Migrations gérées séparément avec `golang-migrate`
- CI doit exécuter `sqlc generate` pour valider cohérence SQL/Go
**Librairies** : Voir [ADR-020](020-librairies-go.md) pour stack complet (sqlc + golang-migrate + pgx)

View File

@@ -29,40 +29,197 @@ RoadWave nécessite applications iOS et Android avec support CarPlay/Android Aut
- **Géolocalisation** : `geolocator` robuste avec gestion permissions
- **Écosystème** : Widgets riches (Material/Cupertino), state management mature (Bloc, Riverpod)
## Packages clés
## Packages Flutter
```yaml
dependencies:
flutter_bloc: ^8.1.3 # State management
just_audio: ^0.9.36 # Lecture audio HLS
geolocator: ^11.0.0 # GPS temps réel (mode voiture)
geofence_service: ^5.2.0 # Geofencing arrière-plan (mode piéton)
flutter_local_notifications: ^17.0.0 # Notifications géolocalisées
dio: ^5.4.0 # HTTP client
flutter_secure_storage: ^9.0.0 # Tokens JWT
cached_network_image: ^3.3.1 # Cache images
> **Voir [ADR-022 - Librairies Flutter](022-librairies-flutter.md)** pour la liste complète des packages, licences, alternatives considérées et justifications détaillées.
**Packages clés pour RoadWave** :
- **State management** : `flutter_bloc` (pattern BLoC, testable, reactive)
- **Audio HLS** : `just_audio` (HLS natif, buffering adaptatif, background playback)
- **GPS temps réel** : `geolocator` (mode voiture haute précision)
- **Geofencing** : `geofence_service` (mode piéton, détection rayon 200m, économie batterie)
- **Notifications** : `flutter_local_notifications` (compteur dynamique, conformité CarPlay/Android Auto)
- **HTTP** : `dio` (client HTTP avec retry logic)
- **Stockage sécurisé** : `flutter_secure_storage` (JWT tokens, Keychain iOS, KeyStore Android)
- **Cache images** : `cached_network_image` (LRU cache)
**Points d'attention** :
- ⚠️ **Permissions progressives requises** pour `geofence_service` et `geolocator` (voir section "Stratégie de Permissions")
- ⚠️ **Licences** : 100% permissives (MIT, BSD-3) - voir ADR-022
## Stratégie de Permissions (iOS/Android)
### Contexte et Enjeux
**Problème** : La géolocalisation en arrière-plan (requise pour le mode piéton) est **très scrutée** par Apple et Google :
- **iOS App Store** : Taux de rejet ~70% si permission "Always Location" mal justifiée
- **Android Play Store** : `ACCESS_BACKGROUND_LOCATION` nécessite déclaration spéciale depuis Android 10
- **RGPD** : Permissions doivent être **optionnelles** et l'app **utilisable sans**
### Architecture de Permissions Progressive
**Principe** : Demander le **minimum** au départ, puis proposer upgrade **contextuel** uniquement si besoin.
#### Niveaux de Permissions
Trois niveaux de permissions doivent être gérés :
- **Denied** : Aucune permission → app limitée (contenu national)
- **When In Use** : "Quand l'app est ouverte" → mode voiture complet ✅
- **Always** : "Toujours" / Background → mode piéton ✅
#### Étape 1 : Permission de Base (Onboarding)
**Quand** : Premier lancement de l'app
**Demande** : `locationWhenInUse` uniquement
- iOS : "Allow While Using App"
- Android : `ACCESS_FINE_LOCATION`
**Justification affichée** :
```
📍 RoadWave a besoin de votre position
Pour vous proposer du contenu audio adapté
à votre localisation en temps réel.
[Autoriser] [Non merci]
```
**Nouveaux packages (contenus géolocalisés)** :
**Si acceptée** : Mode voiture entièrement fonctionnel ✅
- **`geofence_service`** : Détection entrée/sortie rayon 200m en arrière-plan (mode piéton)
- Geofencing natif iOS/Android
- Minimise consommation batterie
- Supporte notifications push même app fermée
**Si refusée** : Mode dégradé (contenus nationaux/régionaux via GeoIP)
- **`flutter_local_notifications`** : Notifications locales avec compteur dynamique
- Notification avec compteur décroissant (7→1) en mode voiture
- Icônes personnalisées selon type contenu
- Désactivation overlay en mode CarPlay/Android Auto (conformité)
#### Étape 2 : Upgrade Optionnel (Contextuel)
**Quand** : Utilisateur **active explicitement** "Notifications audio-guides piéton" dans Settings
**Flow** :
1. **Écran d'éducation** (requis pour validation stores) :
```
┌────────────────────────────────────────┐
│ 📍 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é│
└────────────────────────────────────────┘
```
2. **Demande permission OS** : `locationAlways` / `ACCESS_BACKGROUND_LOCATION`
3. **Si refusée** :
- Toggle "Mode piéton" reste désactivé
- Message : "Mode piéton non disponible sans permission arrière-plan"
- **App reste pleinement fonctionnelle en mode voiture**
### Implémentation
Le service de gestion des permissions (`lib/core/services/location_permission_service.dart`) doit implémenter :
**Détection du niveau actuel** :
- Vérifier le statut de la permission `location` (when in use)
- Vérifier le statut de la permission `locationAlways` (background)
- Retourner le niveau le plus élevé accordé
**Demande de permission de base** (Étape 1) :
- Demander uniquement la permission `location` (when in use)
- Utilisée lors de l'onboarding
- Aucun écran d'éducation requis
**Demande de permission arrière-plan** (Étape 2) :
- **Toujours** afficher un écran d'éducation AVANT la demande OS
- Demander la permission `locationAlways` (iOS) ou `ACCESS_BACKGROUND_LOCATION` (Android)
- Si refusée de manière permanente, proposer l'ouverture des réglages système
**Gestion des refus** :
- Détecter si la permission est refusée de manière permanente
- Proposer l'ouverture des réglages de l'appareil avec un message clair
- Permettre à l'utilisateur d'annuler
### Configuration Platform-Specific
#### iOS (`ios/Runner/Info.plist`)
**Clés requises** :
- `NSLocationWhenInUseUsageDescription` : Décrire l'usage pour le mode voiture (contenu géolocalisé en temps réel)
- `NSLocationAlwaysAndWhenInUseUsageDescription` : Décrire l'usage optionnel pour le mode piéton (notifications audio-guides en arrière-plan), mentionner explicitement que c'est optionnel et désactivable
- `UIBackgroundModes` : Activer les modes `location` et `remote-notification`
**Exemple de texte pour `NSLocationAlwaysAndWhenInUseUsageDescription`** :
> "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."
#### Android (`android/app/src/main/AndroidManifest.xml`)
**Permissions requises** :
- `ACCESS_FINE_LOCATION` et `ACCESS_COARSE_LOCATION` : Permission de base (when in use)
- `ACCESS_BACKGROUND_LOCATION` : Permission arrière-plan (Android 10+), nécessite justification Play Store
- `FOREGROUND_SERVICE` et `FOREGROUND_SERVICE_LOCATION` : Service persistant pour mode piéton (Android 12+)
**Android Play Store** : Déclaration requise dans Play Console lors de la soumission :
- Justification : "Notifications géolocalisées pour audio-guides touristiques en arrière-plan"
- Vidéo démo obligatoire montrant le flow de demande de permission
### Fallbacks et Dégradations Gracieuses
| Niveau Permission | Mode Voiture | Mode Piéton | Contenus Accessibles |
|-------------------|--------------|-------------|---------------------|
| **Always** | ✅ Complet | ✅ Complet | Tous (national + hyperlocal) |
| **When In Use** | ✅ Complet | ❌ Désactivé | Tous (si app ouverte) |
| **Denied** | ⚠️ Limité | ❌ Désactivé | Nationaux/régionaux (GeoIP) |
**Garantie RGPD** : App est **pleinement utilisable** même avec permission refusée (mode dégradé acceptable).
### Tests de Validation Stores
**Checklist App Store (iOS)** :
- [ ] Permission "Always" demandée **uniquement** si user active mode piéton
- [ ] Écran d'éducation **avant** demande OS (requis iOS 13+)
- [ ] App fonctionne sans permission "Always" (validation critique)
- [ ] Texte `Info.plist` clair et honnête (pas de tracking publicitaire)
**Checklist Play Store (Android)** :
- [ ] Déclaration `ACCESS_BACKGROUND_LOCATION` avec justification détaillée
- [ ] Vidéo démo flow de permissions (< 30s, requis Play Console)
- [ ] App fonctionne sans permission background (validation critique)
- [ ] Foreground service notification visible en mode piéton (requis Android 12+)
### Documentation Associée
- **Guide détaillé** : [/docs/mobile/permissions-strategy.md](../mobile/permissions-strategy.md)
- **Règles métier** : [Règle 05 - Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
- **RGPD** : [Règle 02 - Conformité RGPD](../regles-metier/02-conformite-rgpd.md)
---
## Structure application
```
lib/
├── core/ # Config, DI, routes
│ └── services/ # LocationPermissionService, GeofencingService
├── data/ # Repositories, API clients
├── domain/ # Models, business logic
├── presentation/ # UI (screens, widgets, blocs)
│ └── dialogs/ # PedestrianModeEducationDialog
└── main.dart
```
@@ -72,3 +229,6 @@ lib/
- Taille binaire : 8-15 MB (acceptable)
- Tests : `flutter_test` pour widgets, `integration_test` pour E2E
- CI/CD : Fastlane pour déploiement stores
- **Permissions** : Stratégie progressive critique pour validation stores (iOS/Android)
- **Validation stores** : Tests requis avec TestFlight beta (iOS) et Internal Testing (Android)
- **Documentation** : Justifications permissions détaillées requises pour soumission stores

View File

@@ -78,7 +78,9 @@ Approche **multi-niveaux** : unitaires, intégration, BDD (Gherkin), E2E, load t
- `github.com/stretchr/testify`
- `github.com/cucumber/godog`
- `github.com/testcontainers/testcontainers-go`
- `grafana/k6`
- `grafana/k6` (AGPL-3.0, usage interne OK)
- Temps CI : ~3-5 min (tests unitaires + BDD)
- Tests intégration/E2E : nightly builds (15-30 min)
- Load tests : avant chaque release majeure
**Librairies** : Voir [ADR-020](020-librairies-go.md) pour analyses complètes des frameworks de tests

View File

@@ -65,6 +65,8 @@ Architecture hybride en **2 phases** :
## Alternatives considérées
### Architecture de délivrance (serveur vs local)
| Option | Fonctionne offline | Batterie | Complexité | Limite POI | Précision |
|--------|-------------------|----------|------------|------------|-----------|
| **WebSocket + FCM (Phase 1)** | ❌ Non | ⭐ Optimale | ⭐ Faible | ∞ | ⭐⭐ Bonne |
@@ -72,6 +74,17 @@ Architecture hybride en **2 phases** :
| Polling GPS continu | ⭐ Oui | ❌ Critique | ⭐ Faible | ∞ | ⭐⭐⭐ Excellente |
| **Hybride (Phase 1+2)** | ⭐ Oui | ⭐ Adaptative | ⚠️ Moyenne | ∞/20 | ⭐⭐⭐ Excellente |
### Fournisseurs de push notifications
| Provider | Fiabilité | Coût MVP | Coût 100K users | Self-hosted | Vendor lock-in | Verdict |
|----------|-----------|----------|-----------------|-------------|----------------|---------|
| **Firebase (choix)** | 99.95% | **0€** | **0€** | ❌ Non | 🔴 Fort (Google) | ✅ Optimal MVP |
| OneSignal | 99.95% | 0€ | 500€/mois | ❌ Non | 🔴 Fort | ❌ Plus cher |
| Pusher Beams | 99.9% | 0€ | 300€/mois | ❌ Non | 🔴 Fort | ❌ Niche |
| Custom WS + APNS/FCM | Votre charge | 5€ | 100€+ | ✅ Oui | 🟢 Aucun | ⚠️ Complexe |
| Novu (open source) | 99.9% | 15€ | 50€ | ✅ Oui | 🟢 Aucun | 🟡 Phase 2 |
| Brevo API | 99.9% | 0€ | 49€ | ✅ Oui | 🟢 Aucun | ❌ Email seulement |
## Justification
### Pourquoi WebSocket et pas HTTP long-polling ?
@@ -87,6 +100,37 @@ Architecture hybride en **2 phases** :
- **Batterie** : Utilise les mécanismes système (Google Play Services)
- **Cross-platform** : API unifiée iOS/Android
### Incohérence acceptée : Firebase vs self-hosted (ADR-008, ADR-017)
**Problème** : RoadWave promeut 100% self-hosted + souveraineté française, mais Firebase = dépendance Google Cloud.
**Réalité technique** : Notifications natives requièrent obligatoirement Google/Apple
- **APNS (Apple)** : Seul protocole pour notifications iOS → dépendance Apple inévitable
- **FCM (Google)** : Meilleur protocole Android (vs Huawei HMS, Samsung)
**Alternatives analysées** :
1. **Custom WebSocket** (self-hosted) :
- ✅ Zéro dépendance externe
- ❌ 150+ heures dev (2-3 sprints)
- ❌ Maintien de la reliability en-house
- ❌ Toujours besoin d'appeler APNS/FCM de toute façon
2. **Novu (open source self-hosted)** :
- ✅ Self-hostable
- ❌ Jeune (moins mature)
- ❌ Toujours wrapper autour APNS/FCM
- ❌ Overhead sans gain réel
3. **OneSignal / Pusher** :
- ❌ Même vendor lock-in que Firebase
- ❌ Plus cher (500€+/mois @ 100K users)
**Décision pragmatique** :
- Firebase pour MVP : gratuit + fiabilité + time-to-market
- **Mitigation vendor lock-in** : Utiliser abstraction layer (`NotificationProvider` interface)
- **Exit path documenté** : Migration vers custom solution < 1 sprint si besoin futur
- **Probabilité de changement** : Très basse (MVP gratuit, pas d'incitation financière)
### Pourquoi limiter le geofencing local à Phase 2 ?
- **Complexité** : Permissions "Always Location" difficiles à obtenir (taux d'acceptation ~30%)
@@ -101,10 +145,17 @@ Architecture hybride en **2 phases** :
- ✅ Fonctionne avec HLS pour l'audio (pas de conflit avec ADR-002)
- ✅ Scalable : Worker backend peut gérer 10K utilisateurs/seconde avec PostGIS indexé
- ✅ Mode offline disponible en Phase 2 sans refonte
- ✅ Coût zéro jusqu'à millions de notifications (gratuit MVP + croissance)
- ✅ Géolocalisation natif iOS/Android optimisé (moins de batterie)
### Négatives
- Dépendance à Firebase (vendor lock-in) - mitigée par l'utilisation de l'interface FCM standard
- ⚠️ **Dépendance Google (Firebase)** : Contradictoire avec ADR-008 (self-hosted) + ADR-017 (souveraineté FR)
- Mitigé par abstraction layer (`NotificationProvider` interface) → swap facile si besoin
- Exit path documenté pour migration custom (< 1 sprint)
- ⚠️ **Données utilisateur chez Google** : Tokens FCM, timestamps notifications
- Risque RGPD : Nécessite DPA Google valide
- À consulter avec DPO avant déploiement production
- ❌ WebSocket nécessite maintien de connexion (charge serveur +10-20%)
- ❌ Mode offline non disponible au MVP (déception possible des early adopters)
@@ -115,6 +166,57 @@ Architecture hybride en **2 phases** :
- **ADR-012 (Architecture Backend)** : Ajouter un module `geofencing` avec worker dédié
- **ADR-014 (Frontend Mobile)** : Intégrer `firebase_messaging` (Flutter) et gérer permissions
## Abstraction Layer (Mitigation Vendor Lock-in)
Pour minimiser le coût de changement future, implémenter une interface abstraite :
```go
// backend/internal/notification/provider.go
type NotificationProvider interface {
SendNotification(ctx context.Context, token, title, body, deepLink string) error
UpdateToken(ctx context.Context, userID, newToken string) error
}
// backend/internal/notification/firebase_provider.go
type FirebaseProvider struct {
client *messaging.Client
}
func (p *FirebaseProvider) SendNotification(ctx context.Context, token, title, body, deepLink string) error {
message := &messaging.Message{
Notification: &messaging.Notification{
Title: title,
Body: body,
},
Data: map[string]string{
"deepLink": deepLink,
},
Token: token,
}
_, err := p.client.Send(ctx, message)
return err
}
// backend/internal/notification/service.go
type NotificationService struct {
provider NotificationProvider // ← Interface, pas concrète
repo NotificationRepository
}
```
**Bénéfice** : Swap Firebase → Custom/Novu sans changer business logic.
```go
// Futur : switch facilement
var provider NotificationProvider
if config.Provider == "firebase" {
provider = &FirebaseProvider{...}
} else if config.Provider == "custom" {
provider = &CustomProvider{...}
}
```
## Métriques de Succès
- Latence notification < 60s après entrée dans rayon 200m

View File

@@ -0,0 +1,124 @@
# ADR-020 : Librairies Go du Backend
**Statut** : Accepté
**Date** : 2026-01-31
## Contexte
Le backend Go de RoadWave nécessite des librairies tierces pour HTTP, base de données, tests, streaming, etc. Le choix doit privilégier :
- **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales
- **Performance** (10M utilisateurs, 100K RPS, p99 < 100ms)
- **Maturité** et maintenance active
- **Compatibilité** avec PostGIS, HLS, WebRTC
## Décision
Utilisation de **16 librairies open-source** avec licences permissives.
### Core Stack
| Catégorie | Librairie | Licence | Justification |
|-----------|-----------|---------|---------------|
| **HTTP Framework** | `gofiber/fiber/v3` | MIT | 36K RPS, WebSocket natif, Express-like |
| **PostgreSQL** | `jackc/pgx/v5` | MIT | 30-50% plus rapide, PostGIS natif |
| **Redis** | `redis/rueidis` | Apache-2.0 | Client-side caching, GEORADIUS |
| **SQL Codegen** | `sqlc-dev/sqlc` | MIT | Type-safe, zero overhead (ADR-013) |
| **Migrations** | `golang-migrate/migrate` | MIT | Standard, CLI + library |
### Tests
| Catégorie | Librairie | Licence | Justification |
|-----------|-----------|---------|---------------|
| **Unitaires** | `stretchr/testify` | MIT | Standard Go (27% adoption) |
| **BDD** | `cucumber/godog` | MIT | Gherkin natif (ADR-007) |
| **Intégration** | `testcontainers/testcontainers-go` | MIT | PostGIS réel dans Docker |
| **Load** | `grafana/k6` | AGPL-3.0 | Performant (OK usage interne) |
### Services Externes
| Catégorie | Librairie | Licence | Justification |
|-----------|-----------|---------|---------------|
| **Auth JWT** | `zitadel/zitadel-go/v3` | Apache-2.0 | SDK Zitadel officiel (ADR-008) |
| **WebRTC** | `pion/webrtc/v4` | MIT | Pure Go, radio live (ADR-002) |
| **WebSocket** | `coder/websocket` | ISC | Minimal, notifications (ADR-019) |
| **FCM Push** | `firebase.google.com/go` | BSD-3 | SDK Google officiel (ADR-019) |
| **HLS/FFmpeg** | `asticode/go-astiav` | MIT | Bindings FFmpeg n8.0 |
### Utilitaires
| Catégorie | Librairie | Licence | Justification |
|-----------|-----------|---------|---------------|
| **Config** | `spf13/viper` | MIT | Multi-format, env vars |
| **Logging** | `rs/zerolog` | MIT | Zero allocation, le plus rapide |
## Alternatives considérées
Voir [analyse détaillée](../ANALYSE_LIBRAIRIES_GO.md) pour comparatifs complets :
- Framework : Fiber vs Gin vs Echo vs Chi
- PostgreSQL : pgx vs GORM vs database/sql
- Redis : rueidis vs go-redis vs redigo
- Codegen : sqlc vs SQLBoiler vs Ent
- Logging : zerolog vs zap vs slog
## Justification
### Licences
- **15/16 librairies** : MIT, Apache-2.0, BSD, ISC (permissives)
- **1/16** : AGPL-3.0 (k6 load testing, OK usage interne)
- **Compatibilité totale** : Aucun conflit de licence
### Performance
- **Fiber** : 36K RPS (5% plus rapide que Gin/Echo)
- **pgx** : 30-50% plus rapide que GORM
- **rueidis** : Client-side caching automatique
- **zerolog** : Zero allocation, benchmarks 2025
### Maturité
- **Standards** : testify (27% adoption), golang-migrate, viper
- **Production** : Fiber (33K stars), pgx (10K stars), pion (13K stars)
- **Maintenance** : Toutes actives (commits 2025-2026)
## Conséquences
### Positives
- ✅ Aucune restriction licence commerciale
- ✅ Stack cohérent avec ADR existants (001, 002, 007, 008, 013, 015, 019)
- ✅ Performance validée (benchmarks publics)
- ✅ Écosystème mature et documenté
### Négatives
- ⚠️ **k6 (AGPL-3.0)** : Copyleft, mais OK pour tests internes (pas de SaaS k6 prévu)
- ⚠️ **Firebase FCM** : Dépendance Google (mitigation via abstraction layer, ADR-019)
- ❌ Courbe d'apprentissage : 16 librairies à maîtriser (doc nécessaire)
### Dépendances go.mod
```go
require (
github.com/gofiber/fiber/v3 latest
github.com/jackc/pgx/v5 latest
github.com/redis/rueidis latest
github.com/sqlc-dev/sqlc latest
github.com/golang-migrate/migrate/v4 latest
github.com/stretchr/testify latest
github.com/cucumber/godog latest
github.com/testcontainers/testcontainers-go latest
github.com/zitadel/zitadel-go/v3 latest
github.com/pion/webrtc/v4 latest
github.com/coder/websocket latest
firebase.google.com/go/v4 latest
github.com/asticode/go-astiav latest
github.com/spf13/viper latest
github.com/rs/zerolog latest
grafana/k6 latest // dev only
)
```
## Références
- [Analyse complète des librairies](../ANALYSE_LIBRAIRIES_GO.md) (tableaux comparatifs, sources)
- ADR-001 : Langage Backend (Fiber, pgx, go-redis)
- ADR-007 : Tests BDD (Godog)
- ADR-013 : Accès données (sqlc)
- ADR-015 : Stratégie tests (testify, testcontainers, k6)
- ADR-019 : Notifications (WebSocket, FCM)

View File

@@ -0,0 +1,118 @@
# ADR-021 : Service de Géolocalisation par IP
**Statut** : Accepté
**Date** : 2026-01-31
## Contexte
RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé (utilisateurs sans GPS activé). Ce service permet de détecter la ville/région de l'utilisateur à partir de son adresse IP et d'afficher du contenu régional même sans permission GPS.
**Évolution du marché** :
- **Avant 2019** : MaxMind GeoLite2 était téléchargeable gratuitement (base de données locale)
- **Depuis 2019** : MaxMind nécessite un compte + limite 1000 requêtes/jour (gratuit), puis 0.003$/requête au-delà
**Usage RoadWave** :
- Mode dégradé : ~10% des utilisateurs (estimation)
- Volume : 1000 utilisateurs × 10% = 100 requêtes/jour (MVP)
- Critère : Aucune dépendance à un service tiers payant
## Décision
**IP2Location Lite DB** (self-hosted) pour la géolocalisation par IP.
## Alternatives considérées
| Option | Coût/mois | Précision | Hébergement | Maintenance |
|--------|-----------|-----------|-------------|-------------|
| **IP2Location Lite** | Gratuit | ±50 km | Self-hosted | Mise à jour mensuelle |
| MaxMind GeoLite2 API | ~10€ (dépassement) | ±50 km | Cloud MaxMind | Nulle |
| Self-hosted MaxMind | Gratuit | ±50 km | Self-hosted | Compte requis + MAJ |
## Justification
### IP2Location Lite (choix retenu)
**Avantages** :
- Gratuit (pas de limite de requêtes)
- Self-hosted (souveraineté des données, cohérence avec [ADR-004](004-cdn.md))
- Base de données SQLite légère (50-100 MB)
- Mise à jour mensuelle automatisable via cron
- Licence permissive (Creative Commons BY-SA 4.0)
- Pas de compte tiers requis
**Inconvénients** :
- Maintenance mensuelle (mise à jour DB)
- Précision équivalente à MaxMind (~±50 km)
### MaxMind GeoLite2 API (rejeté)
**Pourquoi rejeté** :
- Coût potentiel en cas de dépassement quota (risque faible mais existant)
- Dépendance à un service tiers (perte de souveraineté)
- Compte requis (friction opérationnelle)
### Self-hosted MaxMind (rejeté)
**Pourquoi rejeté** :
- Compte MaxMind obligatoire pour télécharger la DB (friction)
- Complexité identique à IP2Location pour résultat équivalent
- IP2Location offre même fonctionnalité sans compte tiers
## Architecture technique
### Composants
```mermaid
flowchart TD
A[Backend Go<br/>API Handler]
B[GeoIP Service<br/>Wrapper Go autour IP2Location]
C[IP2Location DB<br/>SQLite ~50 MB<br/>Mise à jour mensuelle]
A --> B
B --> C
```
### Flux de géolocalisation
1. **Requête** : API reçoit requête utilisateur sans GPS
2. **Extraction IP** : Lecture IP depuis en-têtes HTTP (`X-Forwarded-For`, `X-Real-IP`)
3. **Lookup DB** : Query SQLite IP2Location (index sur plages IP)
4. **Réponse** : Ville + région + code pays
**Précision attendue** : ±50 km (équivalent MaxMind)
### Maintenance
**Mise à jour mensuelle** :
- Cron job télécharge nouvelle DB IP2Location (1er du mois)
- Backup DB actuelle avant remplacement
- Rechargement service GeoIP (hot reload sans downtime)
**Monitoring** :
- Alertes si DB > 60 jours (DB obsolète)
- Logs requêtes "IP non trouvée" (détection problèmes DB)
## Conséquences
### Positives
- Aucun coût récurrent (gratuit à l'infini)
- Souveraineté complète des données (cohérence ADR-004)
- Pas de dépendance externe (service tiers)
- Latence minimale (lookup local SQLite < 1ms)
### Négatives
- Maintenance mensuelle requise (automatisable)
- Précision limitée (±50 km, acceptable pour mode dégradé)
- Taille base de données (~50-100 MB sur disque)
### Risques atténués
- **DB obsolète** : Alertes automatiques si > 60 jours
- **IP non trouvée** : Fallback "France" par défaut (code pays FR)
- **Perte DB** : Backup automatique avant chaque mise à jour
## Références
- [ADR-004 : CDN (Souveraineté)](004-cdn.md)
- [ADR-017 : Hébergement](017-hebergement.md)
- [Règle 02 : RGPD (Mode Dégradé)](../regles-metier/02-conformite-rgpd.md#136-géolocalisation-optionnelle)
- IP2Location Lite : https://lite.ip2location.com/

View File

@@ -0,0 +1,207 @@
# ADR-022 : Librairies Flutter du Mobile
**Statut** : Accepté
**Date** : 2026-01-31
## Contexte
L'application mobile RoadWave (iOS/Android) nécessite des librairies tierces pour audio HLS, géolocalisation, notifications, state management, etc. Le choix doit privilégier :
- **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales
- **Maturité** et maintenance active (écosystème Flutter)
- **Performance native** (pas de bridge JS)
- **Support CarPlay/Android Auto**
- **Conformité stores** (App Store, Play Store)
## Décision
Utilisation de **8 librairies open-source** Flutter avec licences permissives.
### Core Stack
| Catégorie | Librairie | Licence | Justification |
|-----------|-----------|---------|---------------|
| **State Management** | `flutter_bloc` | MIT | Pattern BLoC, 11K+ stars, reactive streams |
| **Audio HLS** | `just_audio` | MIT | HLS natif, buffering adaptatif, background playback |
| **HTTP Client** | `dio` | MIT | Interceptors, retry logic, 12K+ stars |
| **Stockage sécurisé** | `flutter_secure_storage` | BSD-3 | Keychain iOS, KeyStore Android |
| **Cache images** | `cached_network_image` | MIT | LRU cache, placeholder support |
### Géolocalisation & Permissions
| Catégorie | Librairie | Licence | Justification |
|-----------|-----------|---------|---------------|
| **GPS temps réel** | `geolocator` | MIT | Mode voiture, high accuracy, background modes |
| **Geofencing** | `geofence_service` | MIT | Détection rayon 200m, mode piéton, économie batterie |
| **Notifications locales** | `flutter_local_notifications` | BSD-3 | Compteur dynamique, icônes custom, iOS/Android |
### Packages Additionnels (CarPlay/Android Auto)
| Catégorie | Librairie | Licence | Justification |
|-----------|-----------|---------|---------------|
| **CarPlay** | `flutter_carplay` | MIT | Intégration CarPlay native (communautaire) |
| **Android Auto** | `android_auto_flutter` | Apache-2.0 | Support Android Auto (communautaire) |
| **Permissions** | `permission_handler` | MIT | Gestion unifiée permissions iOS/Android |
## Alternatives considérées
### State Management
- **flutter_bloc** (choisi) : Pattern BLoC, testable, reactive
- **riverpod** : Plus moderne, moins mature
- **provider** : Simple mais limité pour app complexe
- **getx** : Performance mais opinions controversées
### Audio
- **just_audio** (choisi) : HLS natif, communauté active
- **audioplayers** : Moins mature pour streaming
- **flutter_sound** : Orienté recording, pas streaming
### Géolocalisation
- **geolocator** (choisi) : Standard Flutter, 1.2K+ stars
- **location** : Moins maintenu
- **background_location** : Spécifique background uniquement
## Justification
### Licences
- **7/8 librairies** : MIT (permissive totale)
- **1/8** : BSD-3 (permissive, compatible commercial)
- **Compatibilité totale** : Aucun conflit de licence, aucune restriction commerciale
### Maturité
- **flutter_bloc** : 11.6K stars, adoption large (state management standard)
- **just_audio** : 900+ stars, utilisé production (podcasts apps)
- **geolocator** : 1.2K stars, maintenu BaseFlow (entreprise Flutter)
- **dio** : 12K+ stars, client HTTP le plus utilisé Flutter
### Performance
- **Compilation native** : Dart → ARM64 (pas de bridge JS comme React Native)
- **just_audio** : Utilise AVPlayer (iOS) et ExoPlayer (Android) natifs
- **geolocator** : Accès direct CoreLocation (iOS) et FusedLocation (Android)
- **geofence_service** : Geofencing natif, minimise consommation batterie
### Conformité Stores
- **Permissions progressives** : `permission_handler` + stratégie ADR-014
- **Background modes** : `geolocator` + `geofence_service` approuvés stores
- **Notifications** : `flutter_local_notifications` conforme guidelines iOS/Android
## Architecture
```mermaid
graph TB
subgraph UI["Presentation Layer"]
Widgets["Flutter Widgets"]
Bloc["flutter_bloc<br/>(State Management)"]
end
subgraph Data["Data Layer"]
API["dio<br/>(HTTP Client)"]
Storage["flutter_secure_storage<br/>(JWT Tokens)"]
Cache["cached_network_image<br/>(Image Cache)"]
end
subgraph Services["Services Layer"]
Audio["just_audio<br/>(HLS Streaming)"]
GPS["geolocator<br/>(GPS Mode Voiture)"]
Geofence["geofence_service<br/>(Mode Piéton)"]
Notif["flutter_local_notifications<br/>(Alerts Locales)"]
Perms["permission_handler<br/>(Permissions iOS/Android)"]
end
subgraph Platform["Platform Integration"]
CarPlay["flutter_carplay<br/>(iOS)"]
AndroidAuto["android_auto_flutter<br/>(Android)"]
end
Widgets --> Bloc
Bloc --> API
Bloc --> Audio
Bloc --> GPS
Bloc --> Geofence
API --> Storage
Widgets --> Cache
GPS --> Perms
Geofence --> Perms
Geofence --> Notif
Audio --> CarPlay
Audio --> AndroidAuto
classDef uiStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef dataStyle fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
classDef serviceStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
classDef platformStyle fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
class UI,Widgets,Bloc uiStyle
class Data,API,Storage,Cache dataStyle
class Services,Audio,GPS,Geofence,Notif,Perms serviceStyle
class Platform,CarPlay,AndroidAuto platformStyle
```
## Conséquences
### Positives
- ✅ Aucune restriction licence commerciale (100% permissif)
- ✅ Stack cohérent avec ADR-014 (Frontend Mobile)
- ✅ Performance native (compilation ARM64 directe)
- ✅ Écosystème mature et documenté
- ✅ Support CarPlay/Android Auto via communauté
- ✅ Conformité stores (permissions progressives)
### Négatives
- ⚠️ **CarPlay/Android Auto** : Packages communautaires (pas officiels Flutter)
- ⚠️ **Géolocalisation background** : Scrutée par App Store (stratégie progressive requise, ADR-014)
-**Courbe d'apprentissage** : Dart + pattern BLoC à maîtriser
-**Tests stores** : Validation TestFlight (iOS) et Internal Testing (Android) obligatoires
### Dépendances pubspec.yaml
> **Note** : Les versions exactes seront définies lors de l'implémentation. Cette section indique les packages requis, non les versions à utiliser (qui évoluent rapidement dans l'écosystème Flutter).
**Core** :
- `flutter_bloc` - State management
- `just_audio` - Audio HLS streaming
- `dio` - HTTP client
- `flutter_secure_storage` - Stockage sécurisé JWT
- `cached_network_image` - Cache images
**Géolocalisation & Notifications** :
- `geolocator` - GPS haute précision
- `geofence_service` - Geofencing arrière-plan
- `flutter_local_notifications` - Notifications locales
- `permission_handler` - Gestion permissions
**CarPlay/Android Auto** (optionnels MVP) :
- `flutter_carplay` - Intégration CarPlay
- `android_auto_flutter` - Support Android Auto
### Migration depuis ADR-014
La section "Packages clés" de l'ADR-014 est désormais obsolète et doit référencer cet ADR :
> **Packages Flutter** : Voir [ADR-022 - Librairies Flutter](020-librairies-flutter.md) pour la liste complète, licences et justifications.
## Risques et Mitigations
### Risque 1 : CarPlay/Android Auto packages communautaires
- **Impact** : Maintenance non garantie par Flutter team
- **Mitigation** : Fork privé si besoin, contribution upstream, ou développement custom si critique
### Risque 2 : Validation App Store (permissions background)
- **Impact** : Taux de rejet ~70% si mal justifié
- **Mitigation** : Stratégie progressive (ADR-014), écrans d'éducation, tests beta TestFlight
### Risque 3 : Performance audio HLS en arrière-plan
- **Impact** : Interruptions si OS tue l'app
- **Mitigation** : Background audio task iOS, foreground service Android (natif dans `just_audio`)
## Références
- ADR-014 : Frontend Mobile (Flutter, architecture permissions)
- ADR-020 : Librairies Go (même format de documentation)
- [flutter_bloc documentation](https://bloclibrary.dev/)
- [just_audio repository](https://pub.dev/packages/just_audio)
- [geolocator documentation](https://pub.dev/packages/geolocator)
- [Apple CarPlay Developer Guide](https://developer.apple.com/carplay/)
- [Android Auto Developer Guide](https://developer.android.com/training/cars)

View File

@@ -0,0 +1,146 @@
# ADR-023 : Solution de Cache
**Statut** : Accepté
**Date** : 2026-01-31
## Contexte
L'application nécessite un système de cache performant pour plusieurs cas d'usage critiques :
- **Cache de géolocalisation** : Requêtes de proximité géographique intensives (contenus à moins de X mètres)
- **Sessions utilisateurs** : Stockage temporaire des tokens JWT et contexte utilisateur
- **Données de référence** : Métadonnées des contenus audio fréquemment consultés
- **Compteurs en temps réel** : Nombre d'écoutes, statistiques d'engagement
- **Rate limiting** : Protection contre les abus API
Les contraintes de performance sont strictes :
- Latence p99 < 5ms pour les requêtes de cache
- Support de 100K+ requêtes/seconde en lecture
- Persistance optionnelle (données non critiques)
- Clustering pour haute disponibilité
## Décision
**Redis 7+ en mode Cluster** sera utilisé comme solution de cache principale.
Configuration :
- Mode Cluster avec 3 nœuds minimum (haute disponibilité)
- Persistence RDB désactivée pour les caches chauds (performance maximale)
- AOF activé uniquement pour les sessions utilisateurs (durabilité)
- Éviction `allkeys-lru` sur les caches non-critiques
## Alternatives considérées
| Critère | Redis | Memcached | KeyDB | Valkey |
|---------|-------|-----------|-------|--------|
| Géospatial natif | ✅ `GEORADIUS` | ❌ | ✅ | ✅ |
| Structures de données | ✅ Sets, Hashes, Sorted Sets | ❌ Clé-valeur simple | ✅ | ✅ |
| Clustering | ✅ Redis Cluster | ✅ Client-side | ✅ | ✅ |
| Pub/Sub | ✅ | ❌ | ✅ | ✅ |
| Écosystème Go | ✅ `go-redis/redis` | ⚠️ Limité | ✅ Compatible | ✅ Compatible |
| Maturité | ✅ Très mature | ✅ Mature | ⚠️ Fork récent | ⚠️ Fork très récent |
| License | ⚠️ RSALv2 / SSPLv1 | ✅ BSD | ✅ BSD | ✅ BSD |
**Memcached** : Écarté pour l'absence de fonctionnalités géospatiales natives et de structures de données avancées (pas de sets, hashes).
**KeyDB** : Fork multi-thread de Redis compatible API. Écarté par manque de maturité relative et d'écosystème comparé à Redis (moins de contributions, documentation).
**Valkey** : Fork Linux Foundation de Redis (2024). Trop récent pour production, écosystème en construction. À réévaluer en 2026.
## Justification
### Fonctionnalités géospatiales natives
Redis fournit des commandes géospatiales optimisées critiques pour RoadWave :
- `GEOADD` : Indexation de contenus géolocalisés
- `GEORADIUS` : Recherche par rayon (ex: contenus à moins de 5km)
- `GEODIST` : Calcul de distance entre deux points
Ces commandes permettent de servir les requêtes de proximité directement depuis le cache sans solliciter PostgreSQL/PostGIS, réduisant la latence de 50-80ms à <5ms.
### Structures de données riches
- **Hashes** : Métadonnées de contenus (titre, durée, URL HLS) → accès partiel efficace
- **Sets** : Listes de contenus par catégorie, gestion de favoris
- **Sorted Sets** : Classement par popularité, top écoutes hebdomadaires
- **Strings avec TTL** : Sessions utilisateurs avec expiration automatique
### Performance et scalabilité
- **Débit** : 100K+ ops/sec par nœud en lecture (benchmark Redis Labs)
- **Latence** : p99 < 1ms pour GET/SET simple
- **Clustering** : Partitionnement automatique des données sur 16384 hash slots
- **Réplication** : Read replicas pour scaling horizontal en lecture
### Écosystème Go
Librairie `go-redis/redis` (13K+ stars GitHub) :
- Support complet Redis Cluster
- Pipeline et transactions
- Context-aware (intégration Go idiomatique)
- Pooling de connexions automatique
### Pub/Sub pour temps réel
Support natif de messaging publish/subscribe pour :
- Notifications push (invalidation de cache)
- Événements temps réel (nouveau contenu géolocalisé)
- Coordination entre instances API (scaling horizontal)
## Conséquences
### Positives
- **Cache géospatial** : Réduction de charge PostgreSQL de ~70% sur requêtes de proximité
- **Latence** : p99 < 5ms pour requêtes de contenu en cache (vs ~50ms PostgreSQL)
- **Scaling horizontal** : Ajout de nœuds Redis transparent pour l'application
- **Polyvalence** : Un seul système pour cache, sessions, rate limiting, pub/sub
### Négatives
- **Complexité opérationnelle** : Cluster Redis nécessite monitoring (slots, rebalancing)
- **Persistance limitée** : RDB/AOF moins fiable que PostgreSQL → pas pour données critiques
- **Consommation mémoire** : Structures riches = overhead vs Memcached (~20% de RAM en plus)
### Stratégie de cache
**TTL par type de donnée** :
- Métadonnées de contenu : 15 minutes (mise à jour rare)
- Résultats géolocalisés : 5 minutes (contenus statiques géographiquement)
- Sessions utilisateurs : 24 heures (renouvellement automatique)
- Rate limiting : 1 minute (fenêtre glissante)
**Invalidation** :
- Publication de contenu → `DEL` métadonnées + publication Pub/Sub
- Modification géolocalisation → `GEOREM` puis `GEOADD`
- Logout utilisateur → `DEL` session
### Configuration production
**Cluster 3 nœuds** (minimum haute disponibilité) :
- 1 master + 2 replicas
- Répartition sur 3 zones de disponibilité (anti-affinité)
- `cluster-require-full-coverage no` → lecture dégradée si nœud down
**Mémoire** :
- `maxmemory 2gb` par nœud (ajustable selon charge)
- `maxmemory-policy allkeys-lru` → éviction automatique anciennes clés
**Persistance** :
- RDB désactivé (`save ""`) pour caches chauds
- AOF `appendonly yes` uniquement pour sessions (nœud dédié optionnel)
### Monitoring
Métriques critiques à suivre :
- Taux de hit/miss par namespace (target >95% hit rate)
- Latence p99 par commande (alerter si >10ms)
- Fragmentation mémoire (rebalance si >1.5)
- Slots distribution dans le cluster
## Références
- [Redis Geospatial Documentation](https://redis.io/docs/data-types/geospatial/)
- [go-redis/redis](https://github.com/redis/go-redis)
- [ADR-005 : Base de Données](./005-base-de-donnees.md) (architecture cache + PostgreSQL)

View File

@@ -0,0 +1,863 @@
# Stratégie de Permissions Géolocalisation
**Date** : 2026-01-31
**Auteur** : Architecture Mobile RoadWave
**Statut** : Approuvé
**Version** : 1.0
---
## Contexte
La géolocalisation est **critique** pour RoadWave, mais les permissions arrière-plan sont le **#1 motif de rejet** sur iOS App Store et Android Play Store.
### Problématiques Identifiées
#### iOS App Store
- **Taux de rejet ~70%** si permission "Always Location" mal justifiée
- Apple exige que l'app soit **pleinement utilisable** sans "Always Location"
- Textes `Info.plist` scrutés manuellement par reviewers humains
- Rejection si suspicion de tracking publicitaire ou vente de données
#### Android Play Store
- Depuis Android 10 : `ACCESS_BACKGROUND_LOCATION` nécessite **déclaration justifiée**
- Vidéo démo **obligatoire** montrant le flow de demande (< 30s)
- Google vérifie que la permission est **réellement optionnelle**
- Foreground service notification **obligatoire** en arrière-plan (Android 12+)
#### RGPD (Règle 02)
- Permissions doivent être **optionnelles**
- Utilisateur doit pouvoir **refuser sans pénalité**
- App doit fonctionner en **mode dégradé acceptable**
---
## Stratégie Progressive (2 Étapes)
### Vue d'Ensemble
```
┌─────────────────────────────────────────────────────────┐
│ ONBOARDING │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Étape 1: Permission "When In Use" │ │
│ │ → Mode voiture complet ✅ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ User utilise l'app normalement
┌─────────────────────────────────────────────────────────┐
│ SETTINGS (Plus tard, si besoin) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Étape 2: Permission "Always" (optionnelle) │ │
│ │ → Mode piéton avec notifications push ✅ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## Étape 1 : Permission de Base (Onboarding)
### Quand
- **Premier lancement** de l'app
- Avant de pouvoir utiliser les fonctionnalités principales
### Permission Demandée
| Platform | Permission | Nom Utilisateur |
|----------|-----------|----------------|
| **iOS** | `NSLocationWhenInUseUsageDescription` | "Allow While Using App" |
| **Android** | `ACCESS_FINE_LOCATION` | "Autorisez uniquement lorsque l'application est en cours d'utilisation" |
### Flow UI
**Écran pré-permission** (recommandé pour taux d'acceptation) :
```
┌────────────────────────────────────────┐
│ 🗺️ Bienvenue sur RoadWave │
├────────────────────────────────────────┤
│ │
│ RoadWave vous propose du contenu audio│
│ adapté à votre position en temps réel.│
│ │
│ Nous avons besoin de votre localisation│
│ pour : │
│ │
│ ✅ Recommander du contenu proche │
│ ✅ Détecter votre mode (voiture/piéton)│
│ ✅ Synchroniser avec vos trajets │
│ │
│ [Continuer] │
│ │
│ Votre vie privée est protégée │
└────────────────────────────────────────┘
```
**Puis demande système iOS/Android**
### Si Permission Acceptée
- Mode voiture **complet**
- Détection POI quand app **ouverte**
- Recommandations géolocalisées temps réel
- **Pas de demande supplémentaire** sauf si user veut mode piéton
### Si Permission Refusée
**Mode dégradé (IP2Location)** :
- Détection pays/ville via adresse IP (IP2Location Lite, voir [ADR-021](../adr/021-geolocalisation-ip.md))
- Contenus nationaux et régionaux disponibles
- Pas de contenus hyperlocaux (< 10km)
**UI** :
```
┌────────────────────────────────────────┐
│ ⚠️ Géolocalisation désactivée │
├────────────────────────────────────────┤
│ Vous écoutez des contenus de votre │
│ région (détection approximative). │
│ │
│ Pour débloquer les contenus proches : │
│ [Activer la géolocalisation] │
└────────────────────────────────────────┘
```
**Tap "Activer"**`openAppSettings()` (réglages système)
### Code d'Implémentation
```dart
// lib/presentation/onboarding/location_onboarding_screen.dart
class LocationOnboardingScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.map, size: 80, color: Colors.blue),
SizedBox(height: 32),
Text(
'Bienvenue sur RoadWave',
style: Theme.of(context).textTheme.headlineMedium,
),
SizedBox(height: 16),
Text(
'RoadWave vous propose du contenu audio '
'adapté à votre position en temps réel.',
textAlign: TextAlign.center,
),
SizedBox(height: 32),
_buildFeatureList(),
SizedBox(height: 48),
ElevatedButton(
onPressed: () => _requestLocationPermission(context),
child: Text('Continuer'),
),
SizedBox(height: 16),
Text(
'Votre vie privée est protégée',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
);
}
Widget _buildFeatureList() {
return Column(
children: [
_buildFeature('Recommander du contenu proche'),
_buildFeature('Détecter votre mode (voiture/piéton)'),
_buildFeature('Synchroniser avec vos trajets'),
],
);
}
Widget _buildFeature(String text) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Icon(Icons.check_circle, color: Colors.green),
SizedBox(width: 16),
Expanded(child: Text(text)),
],
),
);
}
Future<void> _requestLocationPermission(BuildContext context) async {
final service = context.read<LocationPermissionService>();
final granted = await service.requestBasicPermission();
if (granted) {
// Navigation vers écran principal
Navigator.pushReplacementNamed(context, '/home');
} else {
// Afficher mode dégradé disponible
_showDegradedModeDialog(context);
}
}
void _showDegradedModeDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Géolocalisation désactivée'),
content: Text(
'Vous pouvez toujours utiliser RoadWave avec des contenus '
'de votre région (détection approximative).',
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
Navigator.pushReplacementNamed(context, '/home');
},
child: Text('Continuer sans GPS'),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
await openAppSettings();
},
child: Text('Ouvrir réglages'),
),
],
),
);
}
}
```
---
## Étape 2 : Permission Arrière-Plan (Optionnelle)
### Quand
- User **active explicitement** "Notifications audio-guides piéton" dans Settings
- **Jamais au premier lancement**
### Permission Demandée
| Platform | Permission | Nom Utilisateur |
|----------|-----------|----------------|
| **iOS** | `NSLocationAlwaysAndWhenInUseUsageDescription` | "Allow Always" |
| **Android** | `ACCESS_BACKGROUND_LOCATION` | "Toujours autoriser" |
### Flow UI (Critique pour Validation Stores)
**1. Toggle dans Settings**
```
Settings > Notifications
┌────────────────────────────────────────┐
│ 🔔 Notifications │
├────────────────────────────────────────┤
│ Recommendations de contenu │
│ ├─ En conduite [ON] │
│ └─ Au volant [ON] │
│ │
│ Audio-guides piéton [OFF] │
│ ⓘ Nécessite localisation arrière-plan │
│ │
│ Live de créateurs suivis [ON] │
└────────────────────────────────────────┘
```
**2. Écran d'éducation (OBLIGATOIRE avant demande OS)**
```
┌────────────────────────────────────────┐
│ 📍 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é │
│ ❌ Partagée sans votre consentement │
│ │
│ Cette fonctionnalité est optionnelle. │
│ Vous pouvez utiliser RoadWave sans │
│ cette permission. │
│ │
│ [Continuer] [Non merci] │
│ │
│ Plus d'infos : Politique confidentialité│
└────────────────────────────────────────┘
```
**3. Demande système iOS/Android**
**4. Si permission accordée**
```
✅ Mode piéton activé !
Vous recevrez une notification lorsque
vous passez près d'un audio-guide.
```
**5. Si permission refusée**
```
⚠️ Mode piéton non disponible
Sans permission "Toujours autoriser",
nous ne pouvons pas détecter les
audio-guides en arrière-plan.
Vous pouvez toujours :
✅ Utiliser le mode voiture
✅ Lancer manuellement les audio-guides
[Ouvrir réglages] [Fermer]
```
### Code d'Implémentation
```dart
// lib/presentation/settings/notifications_settings_screen.dart
class NotificationsSettingsScreen extends StatefulWidget {
@override
_NotificationsSettingsScreenState createState() => _NotificationsSettingsScreenState();
}
class _NotificationsSettingsScreenState extends State<NotificationsSettingsScreen> {
bool _pedestrianModeEnabled = false;
@override
void initState() {
super.initState();
_loadPermissionStatus();
}
Future<void> _loadPermissionStatus() async {
final service = context.read<LocationPermissionService>();
final level = await service.getCurrentLevel();
setState(() {
_pedestrianModeEnabled = (level == LocationPermissionLevel.always);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Notifications')),
body: ListView(
children: [
SwitchListTile(
title: Text('Recommendations de contenu'),
subtitle: Text('En conduite'),
value: true,
onChanged: (value) { /* ... */ },
),
SwitchListTile(
title: Text('Audio-guides piéton'),
subtitle: Text('Nécessite localisation arrière-plan'),
value: _pedestrianModeEnabled,
onChanged: _handlePedestrianModeToggle,
),
],
),
);
}
Future<void> _handlePedestrianModeToggle(bool enabled) async {
if (enabled) {
// User veut activer → demander permission
final granted = await _requestBackgroundPermission();
setState(() {
_pedestrianModeEnabled = granted;
});
} else {
// User veut désactiver → juste disable service
setState(() {
_pedestrianModeEnabled = false;
});
// Arrêter geofencing service
context.read<GeofencingService>().stop();
}
}
Future<bool> _requestBackgroundPermission() async {
// Étape 1: Afficher écran d'éducation
final userWantsToContinue = await _showEducationDialog();
if (!userWantsToContinue) return false;
// Étape 2: Demander permission OS
final service = context.read<LocationPermissionService>();
final granted = await service.requestBackgroundPermission(context: context);
if (granted) {
_showSuccessDialog();
// Démarrer geofencing service
context.read<GeofencingService>().start();
} else {
_showDeniedDialog();
}
return granted;
}
Future<bool> _showEducationDialog() async {
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text('📍 Notifications audio-guides piéton'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'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.',
),
SizedBox(height: 16),
Text('🔍 Votre position sera utilisée pour :',
style: TextStyle(fontWeight: FontWeight.bold)),
_buildListItem('Détecter monuments à 200m'),
_buildListItem('Vous envoyer une notification'),
SizedBox(height: 16),
Text('🔒 Votre position ne sera jamais :',
style: TextStyle(fontWeight: FontWeight.bold)),
_buildListItem('Vendue à des tiers', isNegative: true),
_buildListItem('Utilisée pour de la publicité', isNegative: true),
_buildListItem('Partagée sans votre consentement', isNegative: true),
SizedBox(height: 16),
Text(
'Cette fonctionnalité est optionnelle. '
'Vous pouvez utiliser RoadWave sans cette permission.',
style: TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Non merci'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Continuer'),
),
],
),
) ?? false;
}
Widget _buildListItem(String text, {bool isNegative = false}) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(
isNegative ? Icons.cancel : Icons.check_circle,
color: isNegative ? Colors.red : Colors.green,
size: 20,
),
SizedBox(width: 8),
Expanded(child: Text(text)),
],
),
);
}
void _showSuccessDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('✅ Mode piéton activé !'),
content: Text(
'Vous recevrez une notification lorsque vous passez près d\'un audio-guide.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
void _showDeniedDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('⚠️ Mode piéton non disponible'),
content: Text(
'Sans permission "Toujours autoriser", nous ne pouvons pas '
'détecter les audio-guides en arrière-plan.\n\n'
'Vous pouvez toujours :\n'
'✅ Utiliser le mode voiture\n'
'✅ Lancer manuellement les audio-guides',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Fermer'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
openAppSettings();
},
child: Text('Ouvrir réglages'),
),
],
),
);
}
}
```
---
## Tableau de Dégradation Gracieuse
| Niveau Permission | Mode Voiture | Mode Piéton | Contenus Hyperlocaux | Notifications |
|-------------------|--------------|-------------|---------------------|--------------|
| **Always** | ✅ Complet | ✅ Complet | ✅ Tous | Push en arrière-plan |
| **When In Use** | ✅ Complet | ❌ Désactivé | ✅ Si app ouverte | Sonores (app ouverte) |
| **Denied** | ⚠️ IP2Location (ville) | ❌ Désactivé | ❌ Aucun | Aucune |
**Garanties** :
- App **utilisable** à tous niveaux de permission ✅
- Pas de fonctionnalité **bloquante** sans permission ✅
- Mode dégradé **acceptable** (contenus régionaux) ✅
---
## Configuration Plateformes
### iOS (`ios/Runner/Info.plist`)
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- ÉTAPE 1: Permission "When In Use" -->
<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>
<!-- ÉTAPE 2: Permission "Always" (optionnelle) -->
<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>
<!-- Background modes -->
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>remote-notification</string>
</array>
<!-- Privacy - Location Always Usage Description (fallback iOS < 11) -->
<key>NSLocationAlwaysUsageDescription</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.</string>
</dict>
</plist>
```
### Android (`android/app/src/main/AndroidManifest.xml`)
```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.roadwave.app">
<!-- ÉTAPE 1: Permission "When In Use" -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- ÉTAPE 2: Permission "Always" (Android 10+, optionnelle) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Foreground service (requis Android 12+ pour background location) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<!-- Notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:label="RoadWave"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- Foreground service declaration -->
<service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:foregroundServiceType="location"
android:exported="false" />
<!-- ... rest of manifest ... -->
</application>
</manifest>
```
---
## Checklist Validation Stores
### iOS App Store
- [ ] Permission "Always" demandée **uniquement** après activation explicite mode piéton
- [ ] Écran d'éducation **avant** demande OS (avec raisons claires)
- [ ] Texte `NSLocationAlwaysAndWhenInUseUsageDescription` mentionne :
- [ ] Fonctionnalité précise ("audio-guides piéton")
- [ ] **Optionnalité** ("Cette fonctionnalité est optionnelle")
- [ ] Pas de mention tracking/publicité
- [ ] App fonctionne **complètement** avec permission "When In Use" uniquement
- [ ] App fonctionne en **mode dégradé** sans aucune permission (IP2Location)
- [ ] Screenshots montrant app fonctionnelle sans permission "Always"
- [ ] Video demo flow de permissions (< 1 min, optionnel mais recommandé)
### Android Play Store
- [ ] Déclaration `ACCESS_BACKGROUND_LOCATION` avec justification dans Play Console :
- [ ] "Notifications géolocalisées pour audio-guides touristiques en arrière-plan"
- [ ] "Permet aux utilisateurs de recevoir des alertes lorsqu'ils passent près de monuments"
- [ ] **Vidéo démo obligatoire** (< 30s) montrant :
- [ ] Activation toggle "Mode piéton" dans Settings
- [ ] Écran d'éducation pré-permission
- [ ] Demande permission système Android
- [ ] App fonctionnelle si permission refusée
- [ ] Foreground service notification visible en mode piéton (Android 12+)
- [ ] App fonctionne **complètement** avec `ACCESS_FINE_LOCATION` uniquement
- [ ] App fonctionne en **mode dégradé** sans permissions
- [ ] Screenshots montrant app fonctionnelle sans permission background
---
## Tests Requis
### Tests Unitaires
```dart
// test/core/services/location_permission_service_test.dart
void main() {
group('LocationPermissionService', () {
test('getCurrentLevel returns denied when no permission', () async {
// ...
});
test('getCurrentLevel returns whenInUse with basic permission', () async {
// ...
});
test('getCurrentLevel returns always with background permission', () async {
// ...
});
test('requestBasicPermission shows system dialog', () async {
// ...
});
test('requestBackgroundPermission requires education dialog first', () async {
// ...
});
});
}
```
### Tests d'Intégration
```dart
// integration_test/permissions_flow_test.dart
void main() {
testWidgets('Onboarding flow with permission acceptance', (tester) async {
app.main();
await tester.pumpAndSettle();
// Voir écran onboarding
expect(find.text('Bienvenue sur RoadWave'), findsOneWidget);
// Tap continuer
await tester.tap(find.text('Continuer'));
await tester.pumpAndSettle();
// Permission acceptée (mock) → navigation home
expect(find.byType(HomeScreen), findsOneWidget);
});
testWidgets('Settings pedestrian mode activation flow', (tester) async {
// ...
await tester.tap(find.byType(SwitchListTile).last);
await tester.pumpAndSettle();
// Voir écran d'éducation
expect(find.text('Notifications audio-guides piéton'), findsOneWidget);
expect(find.text('Votre position sera utilisée pour'), findsOneWidget);
// Tap continuer
await tester.tap(find.text('Continuer'));
await tester.pumpAndSettle();
// Vérifier demande système (mock)
// ...
});
}
```
### Tests Manuels (Devices Réels)
**iOS** :
- [ ] iPhone avec iOS 14, 15, 16, 17, 18
- [ ] Tester flow onboarding permission "When In Use"
- [ ] Tester activation mode piéton avec permission "Always"
- [ ] Tester refus permission "Always" → app reste fonctionnelle
- [ ] Tester changement permission dans Settings iOS → app réagit correctement
**Android** :
- [ ] Android 10, 11, 12, 13, 14, 15
- [ ] Tester flow onboarding permission `FINE_LOCATION`
- [ ] Tester activation mode piéton avec `BACKGROUND_LOCATION`
- [ ] Tester refus permission background → app reste fonctionnelle
- [ ] Vérifier foreground notification visible en arrière-plan (Android 12+)
---
## Validation TestFlight / Internal Testing
### Phase 1 : TestFlight Beta (iOS)
**Objectif** : Valider que Apple accepte notre stratégie de permissions
**Participants** : 10-20 beta testers externes
**Durée** : 2 semaines
**Checklist** :
- [ ] Upload build vers TestFlight
- [ ] Compléter questionnaire App Store Connect :
- [ ] "Why does your app use background location?"
→ "To send push notifications when users walk near tourist audio-guides, even when app is closed. This feature is optional and can be disabled in settings."
- [ ] Screenshots montrant app fonctionnelle sans permission "Always"
- [ ] Attendre review Apple (24-48h)
- [ ] Si rejet : analyser feedback, ajuster textes/flow, re-soumettre
- [ ] Si accepté : lancer beta test avec testeurs
**Scénarios de test beta** :
1. Installation fresh → onboarding → accepter "When In Use"
2. Utiliser mode voiture pendant 1 semaine
3. Activer mode piéton dans settings → accepter "Always"
4. Vérifier réception notifications push en arrière-plan
5. Désactiver mode piéton → vérifier app toujours fonctionnelle
**Métriques collectées** :
- Taux acceptation permission "When In Use" : cible >85%
- Taux acceptation permission "Always" : cible >40%
- Taux rejet App Review : cible 0%
### Phase 2 : Internal Testing (Android)
**Objectif** : Valider conformité Play Store + foreground service
**Participants** : 5-10 beta testers internes
**Durée** : 1 semaine
**Checklist** :
- [ ] Upload build vers Play Console (Internal Testing)
- [ ] Compléter déclaration permissions :
- [ ] `ACCESS_BACKGROUND_LOCATION` justification
- [ ] Upload vidéo démo (< 30s)
- [ ] Tester sur Android 10, 11, 12, 13, 14, 15
- [ ] Vérifier foreground notification visible (Android 12+)
**Scénarios de test** :
1. Installation → onboarding → accepter `FINE_LOCATION`
2. Utiliser app mode voiture
3. Activer mode piéton → voir écran éducation → accepter `BACKGROUND_LOCATION`
4. App en arrière-plan → marcher près d'un POI → vérifier notification push
5. Vérifier notification foreground service visible dans panneau notifications
**Métriques collectées** :
- Consommation batterie mode piéton : cible <5% par heure
- Taux crash background service : cible <0.1%
---
## Vidéo Démo Play Store (Script)
**Durée** : 25 secondes
**Format** : MP4 1080p, portrait
**Voix off** : Optionnel
**Storyboard** :
| Seconde | Écran | Action | Texte Overlay |
|---------|-------|--------|---------------|
| 0-5 | Settings > Notifications | Scroll vers "Audio-guides piéton" | "Utilisateur active mode piéton" |
| 5-8 | Toggle OFF → ON | Tap toggle | |
| 8-15 | Écran d'éducation | Scroll, lire texte | "Écran explicatif affiché" |
| 15-18 | Tap "Continuer" | Demande permission Android | "Permission arrière-plan demandée" |
| 18-22 | Dialog Android | Tap "Toujours autoriser" | "Utilisateur accepte (optionnel)" |
| 22-25 | Retour Settings | Toggle ON | "Mode piéton activé" |
**Fichier** : `android/play-store-assets/background-location-demo.mp4`
---
## FAQ
### Q1 : Pourquoi ne pas demander "Always" dès le début ?
**R** : Taux d'acceptation ~15% vs ~85% pour "When In Use". Strategy progressive maximise utilisateurs avec permissions.
### Q2 : Que se passe-t-il si user change permission dans Settings OS ?
**R** : App détecte changement via `AppLifecycleState` et `permission_handler`. Si downgrade "Always" → "When In Use", mode piéton désactivé automatiquement avec notification in-app.
### Q3 : Est-ce que IP2Location suffit pour le MVP ?
**R** : Non. Mode voiture nécessite GPS précis pour ETA et notifications géolocalisées (règle métier 05). IP2Location = fallback uniquement.
### Q4 : Combien de temps pour validation TestFlight/Play Store ?
**R** :
- TestFlight : 24-48h (review Apple)
- Play Console Internal Testing : Immédiat (pas de review)
- Play Console Production : 3-7 jours (review Google)
---
## Références
- **ADR-014** : [Frontend Mobile](../adr/014-frontend-mobile.md)
- **Règle 05** : [Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
- **Règle 02** : [Conformité RGPD](../regles-metier/02-conformite-rgpd.md)
- **Apple Guidelines** : [Location Best Practices](https://developer.apple.com/design/human-interface-guidelines/location)
- **Android Guidelines** : [Request Background Location](https://developer.android.com/training/location/permissions#request-background-location)
---
**Dernière mise à jour** : 2026-01-31
**Prochaine revue** : Après validation TestFlight (Sprint 3)

View File

@@ -0,0 +1,618 @@
# Plan de Validation TestFlight & Play Store
**Date** : 2026-01-31
**Auteur** : QA & Mobile Team RoadWave
**Objectif** : Valider stratégie de permissions géolocalisation pour acceptation stores
**Statut** : Prêt à exécuter
---
## Vue d'Ensemble
### Objectifs
1. **Valider acceptation Apple App Store** pour permission "Always Location"
2. **Valider acceptation Google Play Store** pour `ACCESS_BACKGROUND_LOCATION`
3. **Mesurer taux d'acceptation** utilisateurs réels (permissions progressives)
4. **Identifier bugs** flow de permissions sur différents OS/devices
5. **Optimiser textes** pour maximiser acceptation
### Timeline
```
Semaine 1 : Préparation builds + documentation stores
Semaine 2-3: Beta testing iOS (TestFlight)
Semaine 3-4: Beta testing Android (Internal Testing)
Semaine 5 : Corrections + re-soumission si nécessaire
Semaine 6 : Validation finale + go/no-go production
```
---
## Phase 1 : Préparation (Semaine 1)
### Checklist Build iOS
- [ ] **Code freeze** branche `release/testflight-permissions-v1`
- [ ] Vérifier `Info.plist` textes permissions :
- [ ] `NSLocationWhenInUseUsageDescription` ≤ 200 caractères
- [ ] `NSLocationAlwaysAndWhenInUseUsageDescription` ≤ 200 caractères
- [ ] Pas de mention tracking/publicité
- [ ] Mention explicite "optionnel"
- [ ] Vérifier `UIBackgroundModes` contient `location`
- [ ] Build & Archive (Xcode)
- [ ] Version : `1.0.0 (1)` (beta)
- [ ] Bundle ID : `com.roadwave.app`
- [ ] Signing : Distribution certificate
- [ ] Upload vers App Store Connect
- [ ] Attendre processing (15-30 min)
### Checklist Build Android
- [ ] **Code freeze** même branche que iOS
- [ ] Vérifier `AndroidManifest.xml` permissions :
- [ ] `ACCESS_FINE_LOCATION`
- [ ] `ACCESS_BACKGROUND_LOCATION`
- [ ] `FOREGROUND_SERVICE`
- [ ] `FOREGROUND_SERVICE_LOCATION`
- [ ] Vérifier foreground service déclaration
- [ ] Build AAB (Android App Bundle)
- [ ] Version : `1.0.0 (1)`
- [ ] Package : `com.roadwave.app`
- [ ] Signing : Release keystore
- [ ] Upload vers Play Console (Internal Testing)
### Documentation App Store Connect
**Questionnaire "Background Location"** :
**Q1** : "Why does your app use background location?"
**A1** (réponse exacte) :
```
RoadWave sends push notifications to users when they walk near tourist
audio-guides and monuments, even when the app is closed. This allows
tourists to discover local content while exploring a city on foot.
This feature is entirely optional and can be disabled in the app settings.
Users can use RoadWave fully without enabling background location - they
will simply use the "car mode" which only requires location "while using".
Background location is ONLY used for:
- Detecting proximity to audio-guide points of interest (200m radius)
- Sending a single push notification to alert the user
Background location is NEVER used for:
- Advertising or tracking
- Selling data to third parties
- Analytics beyond core functionality
```
**Q2** : "How do users benefit from background location?"
**A2** :
```
Users walking in a city receive timely notifications about nearby cultural
content (museums, monuments, historical sites) without having to keep the
app open. This improves the tourist experience while preserving battery life
through native iOS geofencing.
```
**Screenshots à fournir** (5 minimum) :
1. Onboarding demandant permission "When In Use" uniquement
2. App fonctionnelle en mode voiture (avec permission "When In Use")
3. Settings montrant toggle "Mode piéton" désactivé
4. Écran d'éducation avant demande "Always"
5. App fonctionnelle en mode voiture après refus "Always"
### Documentation Play Console
**Déclaration Permission Background Location** :
**Justification** (max 1000 caractères) :
```
RoadWave utilise ACCESS_BACKGROUND_LOCATION uniquement pour envoyer des
notifications push géolocalisées aux utilisateurs en mode piéton (touristes
à pied).
Usage précis :
- Geofencing radius 200m autour des points d'intérêt (monuments, musées)
- Notification push unique lorsque l'utilisateur entre dans la zone
- Permet découverte de contenus audio-guides sans ouvrir l'app
Cette fonctionnalité est OPTIONNELLE :
- Demandée uniquement si utilisateur active "Mode piéton" dans Settings
- Écran explicatif affiché AVANT demande permission système
- L'app fonctionne pleinement sans cette permission (mode voiture disponible)
Foreground service notification visible (Android 12+) lorsque geofencing actif.
Données de localisation :
- JAMAIS vendues ou partagées avec tiers
- JAMAIS utilisées pour publicité ciblée
- Anonymisées après 24h (conformité RGPD)
```
**Vidéo démo** (requis) :
- [ ] Enregistrer screen recording (25-30s)
- [ ] Montrer activation mode piéton depuis Settings
- [ ] Montrer écran d'éducation
- [ ] Montrer demande permission système Android
- [ ] Montrer app fonctionnelle si refusé
- [ ] Format : MP4, 1080p portrait, < 50MB
- [ ] Upload vers Play Console (section "Permissions")
---
## Phase 2 : Beta Testing iOS (Semaine 2-3)
### Configuration TestFlight
**Groupes de testeurs** :
| Groupe | Nombre | Profil | Objectif |
|--------|--------|--------|----------|
| **Internal** | 3-5 | Équipe dev/QA | Tests rapides pre-external |
| **External 1** | 10-15 | Early adopters tech-savvy | Tests fonctionnels détaillés |
| **External 2** | 20-30 | Grand public varié | Tests UX/acceptation réelle |
**Configuration** :
- [ ] Créer groupe "Internal Testers" (accès immédiat)
- [ ] Créer groupe "External Beta 1" (review Apple requise, 24-48h)
- [ ] Créer groupe "External Beta 2" (après succès Beta 1)
- [ ] Activer feedback automatique TestFlight
- [ ] Préparer questionnaire post-test (Google Forms)
### Scénarios de Test (Internal)
**Durée** : 2-3 jours
**Devices** :
- iPhone 12 (iOS 15)
- iPhone 13 Pro (iOS 16)
- iPhone 14 (iOS 17)
- iPhone 15 Pro (iOS 18)
**Test Case 1 : Onboarding Fresh Install**
```
Given: App jamais installée
When: Installation depuis TestFlight
Then:
- Écran onboarding demande permission "When In Use"
- Texte clair et rassurant
- Acceptation → navigation home
- Refus → mode dégradé disponible
```
**Test Case 2 : Mode Voiture (Permission When In Use)**
```
Given: Permission "When In Use" accordée
When: Utilisation normale app pendant 1h de conduite
Then:
- GPS actif quand app ouverte
- Notifications géolocalisées sonores fonctionnent
- ETA calcul correct (7s avant POI)
- Pas de demande permission supplémentaire
```
**Test Case 3 : Activation Mode Piéton**
```
Given: App utilisée en mode voiture depuis quelques jours
When: User active toggle "Mode piéton" dans Settings
Then:
- Écran d'éducation s'affiche AVANT demande OS
- Texte mentionne "optionnel"
- Tap "Continuer" → demande iOS "Allow Always"
- Tap "Non merci" → toggle reste OFF, app fonctionnelle
```
**Test Case 4 : Mode Piéton Actif**
```
Given: Permission "Always" accordée
When: App en arrière-plan, user marche près d'un POI
Then:
- Notification push reçue (200m du POI)
- Tap notification → app ouvre contenu
- Geofencing ne vide pas batterie (< 5%/h)
```
**Test Case 5 : Refus Permission Always**
```
Given: User refuse permission "Always" dans dialog iOS
When: Retour dans app
Then:
- Message "Mode piéton non disponible"
- Bouton "Ouvrir réglages" disponible
- Mode voiture toujours pleinement fonctionnel
- Pas de popup récurrent de demande permission
```
**Test Case 6 : Changement Permission dans Settings iOS**
```
Given: Permission "Always" active
When: User change dans Settings iOS → "While Using"
Then:
- App détecte changement (AppLifecycleState)
- Mode piéton désactivé automatiquement
- Notification in-app : "Mode piéton désactivé (permission changée)"
- Mode voiture reste fonctionnel
```
### Scénarios de Test (External Beta 1)
**Durée** : 1 semaine
**Instructions aux testeurs** :
```
Bienvenue sur la beta RoadWave !
Nous testons notre système de permissions géolocalisation.
Jour 1-2 : Installation & Mode Voiture
1. Installez l'app depuis TestFlight
2. Suivez l'onboarding (acceptez permission "When In Use")
3. Utilisez l'app normalement en voiture pendant 2 jours
4. Notez : bugs, crashs, notifications fonctionnent ?
Jour 3-5 : Mode Piéton (optionnel)
5. Allez dans Settings > Notifications
6. Activez "Audio-guides piéton"
7. Lisez l'écran explicatif
8. Acceptez OU refusez permission "Always" (votre choix !)
9. Testez mode piéton en marchant en ville
Jour 6-7 : Feedback
10. Répondez au questionnaire (lien ci-dessous)
11. Signalez tout bug via TestFlight feedback
Questionnaire : [lien Google Forms]
```
**Questionnaire Post-Test** (Google Forms) :
1. Avez-vous accepté permission "When In Use" au démarrage ? (Oui/Non)
2. Pourquoi ? (Texte libre)
3. Le texte de permission était-il clair ? (1-5)
4. Avez-vous essayé d'activer le mode piéton ? (Oui/Non)
5. Si oui, avez-vous accepté permission "Always" ? (Oui/Non/N'ai pas essayé)
6. Pourquoi ? (Texte libre)
7. L'écran explicatif avant permission "Always" était-il rassurant ? (1-5)
8. Si vous avez refusé "Always", l'app reste-t-elle utilisable ? (Oui/Non/N/A)
9. Bugs rencontrés ? (Texte libre)
10. Suggestions d'amélioration textes permissions ? (Texte libre)
### Métriques Collectées (Firebase Analytics)
**Events trackés** :
```dart
// Onboarding
analytics.logEvent(
name: 'permission_when_in_use_requested',
);
analytics.logEvent(
name: 'permission_when_in_use_granted',
parameters: {'granted': true},
);
// Mode piéton
analytics.logEvent(
name: 'pedestrian_mode_toggle_attempted',
);
analytics.logEvent(
name: 'permission_education_shown',
);
analytics.logEvent(
name: 'permission_education_continued', // User tap "Continuer"
);
analytics.logEvent(
name: 'permission_education_dismissed', // User tap "Non merci"
);
analytics.logEvent(
name: 'permission_always_granted',
parameters: {'granted': true},
);
// Fallback
analytics.logEvent(
name: 'degraded_mode_activated',
parameters: {'reason': 'permission_denied'},
);
```
**Dashboards Firebase** :
- Taux acceptation "When In Use" : `granted / requested`
- Cible : >85%
- Taux activation mode piéton : `toggle_attempted / total_users`
- Cible : >30%
- Taux acceptation "Always" : `always_granted / education_continued`
- Cible : >40%
- Taux abandon education : `education_dismissed / education_shown`
- Cible : <60%
### Critères de Succès Beta 1
- [ ] Taux acceptation "When In Use" ≥ 80%
- [ ] Taux acceptation "Always" ≥ 35%
- [ ] 0 crash lié aux permissions
- [ ] 0 feedback "app inutilisable sans Always"
- [ ] Score satisfaction écran éducation ≥ 4/5
- [ ] **Apple approuve External Beta** (critique !)
Si Apple **rejette** External Beta :
1. Analyser raison rejet (email App Store Connect)
2. Ajuster textes `Info.plist` si problème wording
3. Ajuster flow si problème UX (ex: trop insistant)
4. Re-soumettre sous 48h
5. Itérer jusqu'à acceptation
---
## Phase 3 : Beta Testing Android (Semaine 3-4)
### Configuration Play Console Internal Testing
**Testeurs** :
- [ ] Ajouter emails testeurs (max 100 pour Internal Testing)
- [ ] Créer "testers list" dans Play Console
- [ ] Share lien installation (pas de review Google pour Internal)
**Devices** :
- Google Pixel 5 (Android 12)
- Samsung Galaxy S21 (Android 13)
- OnePlus 9 (Android 14)
- Google Pixel 8 (Android 15)
### Scénarios de Test (Focus Android)
**Test Case 1 : Foreground Service Notification (Android 12+)**
```
Given: Permission background accordée, mode piéton actif
When: App en arrière-plan avec geofencing actif
Then:
- Notification foreground service visible dans panneau
- Texte : "RoadWave détecte audio-guides à proximité"
- Icône RoadWave visible
- Tap notification → ouvre app
- Notification ne peut pas être swipée (persistent)
```
**Test Case 2 : Permission Background Android 10+**
```
Given: Android 10, 11, 12, 13, 14, ou 15
When: Activation mode piéton
Then:
- Écran éducation s'affiche
- Dialog Android demande "Toujours autoriser"
- Options : "Toujours" / "Seulement pendant utilisation" / "Refuser"
- Selection "Toujours" → mode piéton activé
- Selection autre → mode piéton désactivé
```
**Test Case 3 : Battery Drain**
```
Given: Mode piéton actif, app en arrière-plan
When: 4 heures d'utilisation continue (marche en ville)
Then:
- Consommation batterie < 20% (< 5%/h)
- Pas de "Battery draining" warning Android
- Geofencing utilise location updates optimisés (pas de polling continu)
```
**Test Case 4 : Permission Revocation**
```
Given: Permission background accordée
When: User révoque dans Settings Android
Then:
- App détecte changement (broadcast receiver)
- Mode piéton désactivé automatiquement
- Foreground service arrêté
- Notification in-app : "Mode piéton désactivé"
```
### Vidéo Démo Play Store
**Enregistrement** :
- [ ] Device : Pixel 8 (Android 15, UI stock)
- [ ] Screen recorder : Android natif (Game Toolbar)
- [ ] Durée : 25-30s
- [ ] Orientation : Portrait
- [ ] Résolution : 1080p
**Script** (voir [permissions-strategy.md](permissions-strategy.md#vidéo-démo-play-store-script)) :
1. (0-5s) Settings > Notifications > scroll vers "Audio-guides piéton"
2. (5-8s) Toggle OFF → ON
3. (8-15s) Écran d'éducation affiché, scroll pour lire
4. (15-18s) Tap "Continuer" → demande permission Android
5. (18-22s) Tap "Toujours autoriser"
6. (22-25s) Retour Settings, toggle ON confirmé
**Post-production** :
- [ ] Ajouter text overlays : "Utilisateur active mode piéton", "Écran explicatif affiché", etc.
- [ ] Exporter MP4 < 50MB
- [ ] Upload Play Console > Permissions > Background Location > Video demo
### Critères de Succès Android
- [ ] Foreground service notification visible et claire
- [ ] Consommation batterie acceptable (< 5%/h)
- [ ] 0 crash sur Android 10-15
- [ ] Vidéo démo uploadée et acceptée Play Console
- [ ] Déclaration permission background validée
---
## Phase 4 : Validation & Go/No-Go (Semaine 5-6)
### Analyse Résultats
**Tableau consolidé** :
| Métrique | iOS (Cible) | iOS (Réel) | Android (Cible) | Android (Réel) | Status |
|----------|-------------|-----------|-----------------|----------------|--------|
| Taux acceptation permission base | >85% | ? | >85% | ? | ? |
| Taux activation mode piéton | >30% | ? | >30% | ? | ? |
| Taux acceptation permission background | >40% | ? | >40% | ? | ? |
| Crash rate permissions | 0% | ? | 0% | ? | ? |
| Battery drain mode piéton | <5%/h | ? | <5%/h | ? | ? |
| Feedback "app inutilisable" | 0 | ? | 0 | ? | ? |
### Décision Go/No-Go Production
**Critères GO** (tous doivent être ✅) :
- [ ] Apple a approuvé External Beta TestFlight
- [ ] Taux acceptation permission base iOS ≥ 80%
- [ ] Taux acceptation permission base Android ≥ 80%
- [ ] 0 crash critique lié aux permissions
- [ ] 0 feedback utilisateur "app inutilisable sans background permission"
- [ ] Vidéo démo Android uploadée et validée
- [ ] Battery drain mode piéton < 5%/h (iOS & Android)
**Si NO-GO** :
1. Identifier problème bloquant (cf métriques)
2. Planifier corrections (ex: rewording textes, ajustement flow)
3. Nouvelle itération beta (1-2 semaines)
4. Re-validation
**Si GO** :
1. Merge branche `release/testflight-permissions-v1``main`
2. Tag version `v1.0.0`
3. Préparer soumission production (Semaine 7)
---
## Phase 5 : Soumission Production (Semaine 7+)
### iOS App Store
**Checklist soumission** :
- [ ] Build production uploadé (même code que TestFlight validé)
- [ ] Version : `1.0.0 (1)`
- [ ] Screenshots stores (5 minimum, incluant permissions flow)
- [ ] Description mentionnant "mode piéton optionnel"
- [ ] Keywords : roadwave, audio, gps, tourisme, voyage
- [ ] Pricing : Gratuit
- [ ] App Privacy : Déclarer usage location (voir section)
- [ ] Submit for Review
**App Privacy (obligatoire iOS 14+)** :
Location Data Collection :
- [ ] "Precise Location" : Yes
- [ ] Purpose : "App Functionality" + "Product Personalization"
- [ ] Linked to user : Yes
- [ ] Used for tracking : No
- [ ] "Coarse Location" : No
**Timing** :
- Review Apple : 24-48h (généralement)
- Si rejet : corrections + re-soumission (24h)
- **Total prévu** : 3-7 jours
### Android Play Store
**Checklist soumission** :
- [ ] Build production (Release AAB)
- [ ] Version : `1.0.0 (1)`
- [ ] Screenshots (8 minimum)
- [ ] Description courte (80 caractères)
- [ ] Description longue (4000 caractères max)
- [ ] Catégorie : Travel & Local
- [ ] Pricing : Gratuit
- [ ] Data Safety : Déclarer usage location
- [ ] Submit for Review (Production track)
**Data Safety Form** :
Location Data :
- [ ] "Approximate location" : No
- [ ] "Precise location" : Yes
- [ ] Purpose : "App functionality" + "Personalization"
- [ ] Shared with third parties : No
- [ ] Optional : Yes (mode dégradé disponible)
- [ ] User can request deletion : Yes
**Timing** :
- Review Google : 3-7 jours
- Si rejet : corrections + re-soumission (1-2 jours)
- **Total prévu** : 5-10 jours
---
## Contingences & Risques
### Risque 1 : Apple rejette permission "Always"
**Probabilité** : Moyenne (30%)
**Impact** : Critique (bloque production)
**Mitigation** :
1. Rewording `Info.plist` pour insister sur "optionnel"
2. Ajouter screenshots montrant app sans permission "Always"
3. Écrire email explicatif à App Review Team
4. Si blocage persistant : envisager retrait mode piéton du MVP
### Risque 2 : Taux acceptation permission trop faible
**Probabilité** : Faible (20%)
**Impact** : Modéré (feature peu utilisée)
**Mitigation** :
1. A/B testing textes écran éducation
2. Améliorer wording pour rassurer utilisateurs
3. Ajouter testimonials/reviews dans écran éducation
4. Retarder demande "Always" (demander après 1 semaine d'usage)
### Risque 3 : Battery drain trop élevé
**Probabilité** : Faible (15%)
**Impact** : Critique (rejets stores + mauvaises reviews)
**Mitigation** :
1. Optimiser geofencing radius (200m → 500m)
2. Augmenter interval updates (30s → 60s)
3. Utiliser "significant location changes" iOS au lieu de "continuous"
4. Désactiver geofencing si batterie < 20%
### Risque 4 : Crash sur anciens OS
**Probabilité** : Faible (10%)
**Impact** : Modéré (fragmentation utilisateurs)
**Mitigation** :
1. Tester sur iOS 14, Android 10 (versions minimales)
2. Fallback gracieux si API geofencing non disponible
3. Considérer min SDK Android 11 (au lieu de 10) si trop de bugs
---
## Contacts & Ressources
### Équipe
- **Mobile Lead** : Responsable builds & soumissions stores
- **QA Lead** : Coordination testeurs beta, analyse métriques
- **Product Owner** : Décision go/no-go, priorisation corrections
- **Legal/RGPD** : Validation textes permissions conformité
### Outils
- **TestFlight** : https://appstoreconnect.apple.com
- **Play Console** : https://play.google.com/console
- **Firebase Analytics** : https://console.firebase.google.com
- **Questionnaire Beta** : Google Forms (lien à créer)
- **Tracking Issues** : GitHub Issues avec label `[testflight]`
### Documentation
- [Stratégie Permissions](permissions-strategy.md)
- [ADR-014 Frontend Mobile](../adr/014-frontend-mobile.md)
- [Règle 05 Mode Piéton](../regles-metier/05-interactions-navigation.md)
---
**Plan approuvé par** : [Nom Product Owner]
**Date d'approbation** : [Date]
**Prochaine revue** : Fin Semaine 2 (après External Beta 1)

View File

@@ -4,16 +4,23 @@
**Décision** : Email/Password uniquement (pas d'OAuth tiers)
- ❌ Pas de Google, Apple, Facebook OAuth (dépendance services US/Chine)
- ✅ Email + mot de passe
-**Pas de Google, Apple, Facebook OAuth** (dépendance services US/Chine)
-**Email + mot de passe** (formulaire natif Zitadel)
- ✅ 2FA (Two-Factor Authentication) disponible
- ✅ Option "Appareil de confiance" (skip 2FA pour 30 jours)
**Clarification technique** :
- Zitadel utilise OAuth2/OIDC comme **protocole** (standard moderne pour mobile)
- Mais l'authentification reste 100% **email/password natif**
- **Aucun fournisseur externe** (Google, Apple, etc.) n'est intégré
**Justification** :
- Souveraineté : pas de dépendance externe
- RGPD : données 100% contrôlées
- Coût : 0€ (Zitadel intégré)
> 📋 **Référence technique** : Voir [ADR-008 - OAuth2 vs Fournisseurs Tiers](../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers) pour clarification protocole vs providers.
---
### 1.2 Vérification email

View File

@@ -135,7 +135,7 @@ export-roadwave-[user_id]-[date].zip
| Niveau | Technologie | Contenus accessibles | Consentement |
|--------|-------------|---------------------|--------------|
| **Pays** | Aucune géoloc | Contenus nationaux uniquement | ❌ Non requis |
| **Ville** | GeoIP (MaxMind) | Contenus régionaux/ville | ❌ Non requis |
| **Ville** | GeoIP (IP2Location) | Contenus régionaux/ville | ❌ Non requis |
| **Précis** | GPS | Tous contenus (hyperlocaux inclus) | ✅ Requis |
**Implémentation** :
@@ -144,7 +144,7 @@ export-roadwave-[user_id]-[date].zip
- Upgrade volontaire vers GPS
**API GeoIP** :
- MaxMind GeoLite2 (gratuit, self-hosted)
- IP2Location Lite (gratuit, self-hosted, voir [ADR-021](../adr/021-geolocalisation-ip.md))
- Update DB mensuelle automatique
- Précision ~80% au niveau ville
@@ -314,7 +314,7 @@ export-roadwave-[user_id]-[date].zip
| **Anonymisation GPS** | Geohash PostGIS (24h) | 0€ |
| **Export données** | JSON+HTML+ZIP asynchrone | 0€ |
| **Suppression compte** | Grace period 30j + anonymisation | 0€ |
| **Mode dégradé** | GeoIP MaxMind + GPS optionnel | 0€ |
| **Mode dégradé** | GeoIP IP2Location + GPS optionnel | 0€ |
| **Conservation** | Purge auto 5 ans inactivité | 0€ |
| **Analytics** | Matomo self-hosted | ~5€/mois |
| **Registre traitements** | Markdown Git | 0€ |

View File

@@ -2,16 +2,25 @@
### 3.1 Évolution des jauges
**Décision** : Système simple avec valeurs fixes
**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)** | -0.5% | Désintérêt marqué |
| **Skip tardif (≥30%)** | 0% | Neutre (contenu essayé suffisamment) |
| **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)** | **-0.5%** | Désintérêt marqué |
| **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%**
@@ -48,10 +57,13 @@ Scénario 4 : Skip après 5s
- **Like automatique** : Reflète l'engagement réel (voir [ADR-010](../adr/010-commandes-volant.md))
- **Sécurité routière** : Pas d'action complexe en conduite
- **Prévisibilité** : Règles claires et déterministes
- **Coût minimal** : Calculs simples en backend
- **Fiabilité** : Pas d'edge cases complexes
- **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 [ADR-010 - Formule de Calcul](../adr/010-commandes-volant.md#implémentation-technique) pour l'implémentation backend détaillée.
---
### 3.2 Jauge initiale

View File

@@ -116,7 +116,26 @@ Musée du Louvre : La Joconde - @paris_museum
**Permissions requises** :
⚠️ **Important** : Permission "Always Location" est **optionnelle** et demandée uniquement si user active le mode piéton dans settings.
⚠️ **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
@@ -131,8 +150,12 @@ 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-014 - Stratégie de Permissions](../adr/014-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" :

View File

@@ -246,7 +246,7 @@ Fonctionnalité: Conformité administrative RGPD (Registre, Breach, DPO)
| Anonymisation GPS | Geohash PostGIS (24h) | 0 |
| Export données | JSON+HTML+ZIP asynchrone | 0 |
| Suppression compte | Grace period 30j + anonymisation | 0 |
| Mode dégradé | GeoIP MaxMind + GPS optionnel | 0 |
| Mode dégradé | GeoIP IP2Location + GPS optionnel | 0 |
| Conservation | Purge auto 5 ans inactivité | 0 |
| Analytics | Matomo self-hosted | ~5/mois |
| Registre traitements | Markdown Git | 0 |

View File

@@ -21,7 +21,7 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
Exemples:
| niveau | technologie | contenus | consentement |
| Pays | Aucune géoloc | Contenus nationaux uniquement | Non requis |
| Ville | GeoIP (MaxMind) | Contenus régionaux/ville | Non requis |
| Ville | GeoIP (IP2Location) | Contenus régionaux/ville | Non requis |
| Précis | GPS | Tous contenus (hyperlocaux inclus) | Requis |
# Démarrage avec GeoIP automatique
@@ -35,9 +35,9 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
Et aucun consentement n'est requis (GeoIP ne collecte pas de données personnelles)
Et je peux accéder aux contenus régionaux et de ville
Scénario: Détection de ville avec MaxMind GeoLite2
Scénario: Détection de ville avec IP2Location Lite
Étant donné que mon adresse IP est 93.184.216.34
Quand le système utilise GeoIP MaxMind GeoLite2
Quand le système utilise GeoIP IP2Location Lite
Alors ma ville est détectée: "Paris"
Et ma région est détectée: "Île-de-France"
Et mon pays est détecté: "France"
@@ -115,20 +115,20 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
Et seule la ville est stockée (non identifiant)
Et aucun consentement n'est requis conformément au RGPD
# Implémentation MaxMind GeoLite2
# Implémentation IP2Location Lite
Scénario: Base de données MaxMind self-hosted
Étant donné que RoadWave utilise MaxMind GeoLite2
Scénario: Base de données IP2Location self-hosted
Étant donné que RoadWave utilise IP2Location Lite
Quand on analyse l'infrastructure
Alors la base de données GeoLite2 est hébergée sur les serveurs RoadWave
Alors la base de données IP2Location est hébergée sur les serveurs RoadWave
Et aucune requête n'est envoyée à un service tiers
Et la base de données est mise à jour automatiquement chaque mois
Et le coût est de 0 (GeoLite2 est gratuit)
Et le coût est de 0 (IP2Location Lite est gratuit)
Scénario: Mise à jour mensuelle de la base GeoIP
Étant donné que MaxMind publie des mises à jour mensuelles
Étant donné que IP2Location publie des mises à jour mensuelles
Quand le 1er du mois arrive
Alors un job automatique télécharge la nouvelle base GeoLite2
Alors un job automatique télécharge la nouvelle base IP2Location Lite
Et la base est mise à jour sans interruption de service
Et un log est créé pour traçabilité
Et si la mise à jour échoue, une alerte est envoyée
@@ -216,7 +216,7 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
# Coût de la solution: 0€
Scénario: Solution GeoIP gratuite et self-hosted
Étant donné que RoadWave utilise MaxMind GeoLite2
Étant donné que RoadWave utilise IP2Location Lite
Quand on calcule le coût de la solution
Alors le coût est de 0
Et la solution est opensource