feat(ai-training): ajout validation OpenCV, gestion des logs et modération des contributions

- Backend IA :
  - Intégration d'OpenCV (WebAssembly) pour la détection et certification des cibles (anneaux concentriques, confiance).
  - Ajout du système de journalisation complet des uploads (SQLite, IP, appareil, statut, métadonnées).
  - Implémentation du système de modération (bannissement/débannissement de wallets, rejet 403 avec motifs).
  - Harmonisation des codes d'erreur et messages JSON (UPLOAD_SUCCESS, WALLET_BANNED, etc.).
  - Dashboard Next.js : page dédiée aux logs avec filtres, export CSV et actions de modération en un clic.
- Application Mobile (Flutter) :
  - Encapsulation des réponses d'export dans AiExportResult avec gestion fine des erreurs et statuts.
  - Mise à jour du disclaimer de participation à l'entraînement IA avec avertissements stricts de bannissement.
  - Révocation de l'envoi de photos et affichage d'un bandeau explicatif en cas de suspension.
  - Ajout d'un bouton manuel « Actualiser mon statut » dans les paramètres pour synchroniser l'état du compte.
  - Possibilité de configurer l'URL/IP du serveur IA directement depuis les paramètres.
This commit is contained in:
streaper2
2026-08-25 12:16:29 +02:00
parent 0374a7611e
commit 44ac4462a6
14 changed files with 2869 additions and 141 deletions
+10 -7
View File
@@ -241,15 +241,18 @@ class AnalysisProvider extends ChangeNotifier {
/// Exporte l'image et le json vers le backend IA.
/// [sessionId], [distance] et [weapon] proviennent du SessionProvider de
/// l'écran appelant, pour que le dataset contienne les vraies métadonnées.
Future<bool> exportToAiBackend({
Future<AiExportResult> exportToAiBackend({
String? sessionId,
int? distance,
String? weapon,
}) async {
if (_imagePath == null || _targetType == null) {
_errorMessage = "Impossible d'export : image ou type de cible manquant.";
_errorMessage = "Impossible d'exporter : image ou type de cible manquant.";
notifyListeners();
return false;
return AiExportResult.error(
code: 'MISSING_DATA',
message: "Impossible d'exporter : image ou type de cible manquant.",
);
}
final service = AiExportService();
@@ -257,7 +260,7 @@ class AnalysisProvider extends ChangeNotifier {
_state = AnalysisState.loading;
notifyListeners();
final success = await service.exportData(
final result = await service.exportData(
imagePath: _imagePath!,
sessionId: sessionId ?? 'export',
targetType: _targetType!,
@@ -270,11 +273,11 @@ class AnalysisProvider extends ChangeNotifier {
);
_state = AnalysisState.success;
if (!success) {
_errorMessage = "Échec de l'export vers le serveur IA.";
if (!result.isSuccess) {
_errorMessage = result.message;
}
notifyListeners();
return success;
return result;
}
/// Save the session
+65 -15
View File
@@ -19,6 +19,7 @@ import '../../data/repositories/session_repository.dart';
import '../../services/score_calculator_service.dart';
import '../../services/grouping_analyzer_service.dart';
import '../../services/wallet_identity_service.dart';
import '../../services/ai_export_service.dart';
import '../session/session_provider.dart';
import 'analysis_provider.dart';
import 'impact_editor_screen.dart';
@@ -933,12 +934,12 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
date: sessionProvider.sessionDate,
);
bool? exportSucceeded;
AiExportResult? exportResult;
if (export) {
messenger.showSnackBar(
const SnackBar(content: Text('Exportation en cours...')),
const SnackBar(content: Text('Exportation vers le serveur IA en cours...')),
);
exportSucceeded = await provider.exportToAiBackend(
exportResult = await provider.exportToAiBackend(
sessionId: sessionProvider.activeSessionId,
distance: sessionProvider.distance,
weapon: sessionProvider.currentWeapon,
@@ -954,19 +955,68 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
openMainTab(mainTabStats);
}
if (exportSucceeded != null) {
messenger.showSnackBar(
SnackBar(
content: Text(
exportSucceeded
? 'Export réussi vers le backend IA !'
: (provider.errorMessage ?? 'Erreur d\'export'),
if (exportResult != null) {
if (exportResult.isBanned) {
messenger.showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.block, color: Colors.white),
const SizedBox(width: 12),
Expanded(
child: Text(
'Participation IA suspendue : ${exportResult.reason ?? "Non-respect des règles"}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
backgroundColor: AppTheme.errorColor,
duration: const Duration(seconds: 6),
),
backgroundColor: exportSucceeded
? AppTheme.successColor
: AppTheme.errorColor,
),
);
);
} else if (exportResult.isSuccess) {
final targetStatus = exportResult.targetValidation?['status'];
final isCertified = targetStatus == 'VALID';
messenger.showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
isCertified ? Icons.verified : Icons.cloud_done,
color: Colors.white,
),
const SizedBox(width: 12),
Expanded(
child: Text(
isCertified
? 'Export réussi ! Cible certifiée par l\'IA.'
: exportResult.message,
),
),
],
),
backgroundColor: AppTheme.successColor,
duration: const Duration(seconds: 4),
),
);
} else {
messenger.showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.warning_amber_rounded, color: Colors.white),
const SizedBox(width: 12),
Expanded(
child: Text('Échec de l\'export : ${exportResult.message}'),
),
],
),
backgroundColor: AppTheme.errorColor,
duration: const Duration(seconds: 4),
),
);
}
}
} catch (e) {
messenger.showSnackBar(