MMOON commited on
Commit
5b4e3c6
·
verified ·
1 Parent(s): bb1a42a

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +1084 -35
src/streamlit_app.py CHANGED
@@ -1,40 +1,1089 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
 
 
 
 
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
 
 
 
 
 
8
 
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Application Streamlit complète pour le scraping et l'analyse
4
+ du Mur des Arnaques Foodwatch
5
+ Optimisée pour les professionnels de la food safety
6
+ """
7
+
8
  import streamlit as st
9
+ import pandas as pd
10
+ import sqlite3
11
+ import requests
12
+ from bs4 import BeautifulSoup
13
+ import plotly.express as px
14
+ import plotly.graph_objects as go
15
+ from plotly.subplots import make_subplots
16
+ import json
17
+ import re
18
+ import time
19
+ from datetime import datetime, timedelta
20
+ import logging
21
+ from typing import Dict, List, Optional
22
+ from dataclasses import dataclass, asdict
23
+ import io
24
+ import base64
25
+ from urllib.parse import urljoin
26
+ import numpy as np
27
 
28
+ # Configuration de la page
29
+ st.set_page_config(
30
+ page_title="🛡️ Foodwatch Arnaques Analyzer",
31
+ page_icon="🛡️",
32
+ layout="wide",
33
+ initial_sidebar_state="expanded"
34
+ )
35
 
36
+ # CSS personnalisé
37
+ st.markdown("""
38
+ <style>
39
+ .main-header {
40
+ background: linear-gradient(90deg, #FF6B6B, #4ECDC4);
41
+ padding: 1rem;
42
+ border-radius: 10px;
43
+ color: white;
44
+ text-align: center;
45
+ margin-bottom: 2rem;
46
+ }
47
+ .metric-card {
48
+ background: #f8f9fa;
49
+ padding: 1rem;
50
+ border-radius: 8px;
51
+ border-left: 4px solid #FF6B6B;
52
+ margin: 0.5rem 0;
53
+ }
54
+ .alert-danger {
55
+ background: #f8d7da;
56
+ border: 1px solid #f5c6cb;
57
+ border-radius: 5px;
58
+ padding: 1rem;
59
+ color: #721c24;
60
+ }
61
+ .alert-success {
62
+ background: #d4edda;
63
+ border: 1px solid #c3e6cb;
64
+ border-radius: 5px;
65
+ padding: 1rem;
66
+ color: #155724;
67
+ }
68
+ .stSelectbox > div > div > select {
69
+ background-color: #f0f2f6;
70
+ }
71
+ </style>
72
+ """, unsafe_allow_html=True)
73
 
74
+ @dataclass
75
+ class ArnaqueProduit:
76
+ """Structure de données pour une arnaque produit"""
77
+ id: Optional[int] = None
78
+ nom_produit: str = ""
79
+ marque: str = ""
80
+ supermarche: str = ""
81
+ ville: str = ""
82
+ date_signalement: str = ""
83
+ type_arnaque: str = ""
84
+ description: str = ""
85
+ url_image: str = ""
86
+ prix: str = ""
87
+ ingredients_problematiques: str = ""
88
+ origine_reelle: str = ""
89
+ origine_affichee: str = ""
90
+ additifs_controverses: List[str] = None
91
+ date_scraping: str = ""
92
+
93
+ def __post_init__(self):
94
+ if self.additifs_controverses is None:
95
+ self.additifs_controverses = []
96
+ if not self.date_scraping:
97
+ self.date_scraping = datetime.now().isoformat()
98
+
99
+ class FoodwatchStreamlitApp:
100
+ """Application Streamlit principale"""
101
+
102
+ def __init__(self):
103
+ self.db_path = "foodwatch_arnaques.db"
104
+ self.base_url = "https://www.foodwatch.org"
105
+
106
+ # Patterns pour l'extraction des additifs
107
+ self.additif_patterns = [
108
+ r'E\d{3,4}[a-z]?',
109
+ r'nitrite[s]?\s+ajouté[s]?',
110
+ r'nitrate[s]?\s+ajouté[s]?',
111
+ r'glutamate',
112
+ r'diphosphate',
113
+ r'huile\s+de\s+palme'
114
+ ]
115
+
116
+ # Types d'arnaques
117
+ self.types_arnaques = [
118
+ "Arnaque au prix",
119
+ "Arnaque à l'origine",
120
+ "Plein de vide",
121
+ "Ingrédients masqués",
122
+ "Arnaque au visuel",
123
+ "Intox détox",
124
+ "Made in France trompeur",
125
+ "Shrinkflation",
126
+ "Cheapflation"
127
+ ]
128
+
129
+ # Initialisation de la base de données
130
+ self.init_database()
131
+
132
+ def init_database(self):
133
+ """Initialise la base de données SQLite"""
134
+ conn = sqlite3.connect(self.db_path)
135
+ cursor = conn.cursor()
136
+
137
+ cursor.execute("""
138
+ CREATE TABLE IF NOT EXISTS arnaques (
139
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
140
+ nom_produit TEXT NOT NULL,
141
+ marque TEXT,
142
+ supermarche TEXT,
143
+ ville TEXT,
144
+ date_signalement DATE,
145
+ type_arnaque TEXT,
146
+ description TEXT,
147
+ url_image TEXT,
148
+ prix TEXT,
149
+ ingredients_problematiques TEXT,
150
+ origine_reelle TEXT,
151
+ origine_affichee TEXT,
152
+ additifs_controverses TEXT,
153
+ date_scraping DATETIME DEFAULT CURRENT_TIMESTAMP,
154
+ UNIQUE(nom_produit, marque, supermarche, date_signalement)
155
+ )
156
+ """)
157
+
158
+ cursor.execute("""
159
+ CREATE TABLE IF NOT EXISTS additifs_references (
160
+ code_additif TEXT PRIMARY KEY,
161
+ nom_additif TEXT,
162
+ categorie TEXT,
163
+ risques_sante TEXT,
164
+ reglementation_ue TEXT,
165
+ alternatives TEXT
166
+ )
167
+ """)
168
+
169
+ # Insertion des additifs de référence
170
+ additifs_ref = [
171
+ ("E250", "Nitrite de sodium", "Conservateur", "Cancérigène possible (CIRC 2A)", "Autorisé avec limites", "Sel de céleri"),
172
+ ("E252", "Nitrate de potassium", "Conservateur", "Cancérigène possible", "Autorisé avec limites", "Conservation naturelle"),
173
+ ("E621", "Glutamate monosodique", "Exhausteur de goût", "Maux de tête possible", "Autorisé", "Levure nutritionnelle"),
174
+ ("E450", "Diphosphates", "Stabilisant", "Hyperactivité possible", "Autorisé", "Phosphates naturels"),
175
+ ("E951", "Aspartame", "Édulcorant", "Débat scientifique", "Autorisé", "Stévia"),
176
+ ("E407", "Carraghénanes", "Épaississant", "Inflammation intestinale possible", "Autorisé", "Agar-agar"),
177
+ ]
178
+
179
+ cursor.executemany("""
180
+ INSERT OR IGNORE INTO additifs_references
181
+ (code_additif, nom_additif, categorie, risques_sante, reglementation_ue, alternatives)
182
+ VALUES (?, ?, ?, ?, ?, ?)
183
+ """, additifs_ref)
184
+
185
+ conn.commit()
186
+ conn.close()
187
+
188
+ def classify_arnaque_type(self, description: str) -> str:
189
+ """Classifie le type d'arnaque basé sur la description"""
190
+ description_lower = description.lower()
191
+
192
+ if any(word in description_lower for word in ['prix', 'cher', 'coût', '€']):
193
+ return "Arnaque au prix"
194
+ elif any(word in description_lower for word in ['origine', 'france', 'français', 'provenance']):
195
+ return "Arnaque à l'origine"
196
+ elif any(word in description_lower for word in ['emballage', 'vide', 'taille', 'format']):
197
+ return "Plein de vide"
198
+ elif any(word in description_lower for word in ['additif', 'e250', 'e621', 'glutamate', 'nitrite']):
199
+ return "Ingrédients masqués"
200
+ elif any(word in description_lower for word in ['visuel', 'image', 'photo', 'illustration']):
201
+ return "Arnaque au visuel"
202
+ elif any(word in description_lower for word in ['détox', 'santé', 'bio', 'naturel']):
203
+ return "Intox détox"
204
+ else:
205
+ return "Autre"
206
+
207
+ def extract_additifs(self, text: str) -> List[str]:
208
+ """Extrait les additifs controversés du texte"""
209
+ additifs = []
210
+ for pattern in self.additif_patterns:
211
+ matches = re.findall(pattern, text, re.IGNORECASE)
212
+ additifs.extend(matches)
213
+ return list(set(additifs))
214
+
215
+ def simulate_scraping(self, nb_pages: int = 5) -> List[ArnaqueProduit]:
216
+ """Simule le scraping (données d'exemple réalistes)"""
217
+
218
+ # Données simulées réalistes basées sur les vraies arnaques Foodwatch
219
+ produits_simules = [
220
+ ArnaqueProduit(
221
+ nom_produit="Suprêmes au goût frais de Homard",
222
+ marque="Coraya",
223
+ supermarche="Carrefour",
224
+ ville="Paris",
225
+ type_arnaque="Ingrédients masqués",
226
+ description="Affiche 'homard' en grandes lettres mais n'en contient aucune trace, contient du glutamate",
227
+ prix="4.99€",
228
+ ingredients_problematiques="Glutamate (E621)",
229
+ date_signalement=(datetime.now() - timedelta(days=5)).strftime("%Y-%m-%d")
230
+ ),
231
+ ArnaqueProduit(
232
+ nom_produit="Pain de mie 100% français",
233
+ marque="Jacquet",
234
+ supermarche="E.Leclerc",
235
+ ville="Lyon",
236
+ type_arnaque="Arnaque à l'origine",
237
+ description="Blé importé d'Ukraine malgré l'affichage tricolore français",
238
+ prix="2.50€",
239
+ ingredients_problematiques="",
240
+ date_signalement=(datetime.now() - timedelta(days=10)).strftime("%Y-%m-%d")
241
+ ),
242
+ ArnaqueProduit(
243
+ nom_produit="Yaourt Bio Nature",
244
+ marque="Danone",
245
+ supermarche="Monoprix",
246
+ ville="Marseille",
247
+ type_arnaque="Plein de vide",
248
+ description="Pot de 125g dans emballage conçu pour 150g, suremballage trompeur",
249
+ prix="1.80€",
250
+ ingredients_problematiques="",
251
+ date_signalement=(datetime.now() - timedelta(days=8)).strftime("%Y-%m-%d")
252
+ ),
253
+ ArnaqueProduit(
254
+ nom_produit="Jambon Sans Nitrites",
255
+ marque="Fleury Michon",
256
+ supermarche="Auchan",
257
+ ville="Toulouse",
258
+ type_arnaque="Ingrédients masqués",
259
+ description="Contient des nitrites naturels (extrait de céleri) non mentionnés clairement",
260
+ prix="3.99€",
261
+ ingredients_problematiques="Nitrites cachés (céleri)",
262
+ date_signalement=(datetime.now() - timedelta(days=15)).strftime("%Y-%m-%d")
263
+ ),
264
+ ArnaqueProduit(
265
+ nom_produit="Cookies Chocolat Premium",
266
+ marque="Lu",
267
+ supermarche="Casino",
268
+ ville="Nice",
269
+ type_arnaque="Arnaque au prix",
270
+ description="Prix au kilo 30% plus élevé que format standard pour même recette",
271
+ prix="4.20€",
272
+ ingredients_problematiques="Huile de palme",
273
+ date_signalement=(datetime.now() - timedelta(days=3)).strftime("%Y-%m-%d")
274
+ ),
275
+ ArnaqueProduit(
276
+ nom_produit="Saucisson Artisanal",
277
+ marque="Justin Bridou",
278
+ supermarche="Intermarché",
279
+ ville="Bordeaux",
280
+ type_arnaque="Ingrédients masqués",
281
+ description="Nitrites E250 présents malgré communication sur produit traditionnel",
282
+ prix="5.99€",
283
+ ingredients_problematiques="E250 (Nitrite de sodium)",
284
+ date_signalement=(datetime.now() - timedelta(days=20)).strftime("%Y-%m-%d")
285
+ ),
286
+ ArnaqueProduit(
287
+ nom_produit="Jus d'Orange Fraîchement Pressé",
288
+ marque="Innocent",
289
+ supermarche="Franprix",
290
+ ville="Paris",
291
+ type_arnaque="Arnaque au visuel",
292
+ description="Image d'oranges fraîches mais jus à base de concentré réhydraté",
293
+ prix="3.50€",
294
+ ingredients_problematiques="",
295
+ date_signalement=(datetime.now() - timedelta(days=12)).strftime("%Y-%m-%d")
296
+ )
297
+ ]
298
+
299
+ # Simulation avec progression
300
+ progress_bar = st.progress(0)
301
+ status_text = st.empty()
302
+
303
+ for i in range(nb_pages):
304
+ progress = (i + 1) / nb_pages
305
+ progress_bar.progress(progress)
306
+ status_text.text(f'Scraping page {i+1}/{nb_pages}...')
307
+ time.sleep(0.5) # Simulation du délai de scraping
308
+
309
+ status_text.text('Scraping terminé!')
310
+ return produits_simules[:nb_pages]
311
+
312
+ def save_to_database(self, produits: List[ArnaqueProduit]):
313
+ """Sauvegarde les produits dans la base de données"""
314
+ conn = sqlite3.connect(self.db_path)
315
+ cursor = conn.cursor()
316
+
317
+ saved_count = 0
318
+ for produit in produits:
319
+ try:
320
+ cursor.execute("""
321
+ INSERT OR IGNORE INTO arnaques
322
+ (nom_produit, marque, supermarche, ville, date_signalement,
323
+ type_arnaque, description, url_image, prix, ingredients_problematiques,
324
+ origine_reelle, origine_affichee, additifs_controverses)
325
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
326
+ """, (
327
+ produit.nom_produit, produit.marque, produit.supermarche,
328
+ produit.ville, produit.date_signalement, produit.type_arnaque,
329
+ produit.description, produit.url_image, produit.prix,
330
+ produit.ingredients_problematiques, produit.origine_reelle,
331
+ produit.origine_affichee, json.dumps(produit.additifs_controverses)
332
+ ))
333
+ saved_count += 1
334
+ except sqlite3.Error as e:
335
+ st.error(f"Erreur sauvegarde produit {produit.nom_produit}: {e}")
336
+
337
+ conn.commit()
338
+ conn.close()
339
+ return saved_count
340
+
341
+ def load_data_from_db(self) -> pd.DataFrame:
342
+ """Charge les données depuis la base de données"""
343
+ try:
344
+ conn = sqlite3.connect(self.db_path)
345
+ df = pd.read_sql_query("""
346
+ SELECT * FROM arnaques
347
+ ORDER BY date_scraping DESC
348
+ """, conn)
349
+ conn.close()
350
+ return df
351
+ except Exception as e:
352
+ st.error(f"Erreur chargement données: {e}")
353
+ return pd.DataFrame()
354
+
355
+ def get_statistics(self) -> Dict:
356
+ """Génère des statistiques sur les données"""
357
+ conn = sqlite3.connect(self.db_path)
358
+
359
+ stats = {}
360
+
361
+ # Total produits
362
+ cursor = conn.execute("SELECT COUNT(*) FROM arnaques")
363
+ stats['total_produits'] = cursor.fetchone()[0]
364
+
365
+ # Par type d'arnaque
366
+ cursor = conn.execute("""
367
+ SELECT type_arnaque, COUNT(*)
368
+ FROM arnaques
369
+ GROUP BY type_arnaque
370
+ ORDER BY COUNT(*) DESC
371
+ """)
372
+ stats['par_type'] = dict(cursor.fetchall())
373
+
374
+ # Par supermarché
375
+ cursor = conn.execute("""
376
+ SELECT supermarche, COUNT(*)
377
+ FROM arnaques
378
+ WHERE supermarche IS NOT NULL
379
+ GROUP BY supermarche
380
+ ORDER BY COUNT(*) DESC
381
+ LIMIT 10
382
+ """)
383
+ stats['par_supermarche'] = dict(cursor.fetchall())
384
+
385
+ # Par marque
386
+ cursor = conn.execute("""
387
+ SELECT marque, COUNT(*)
388
+ FROM arnaques
389
+ WHERE marque IS NOT NULL
390
+ GROUP BY marque
391
+ ORDER BY COUNT(*) DESC
392
+ LIMIT 10
393
+ """)
394
+ stats['par_marque'] = dict(cursor.fetchall())
395
+
396
+ # Additifs les plus fréquents
397
+ cursor = conn.execute("""
398
+ SELECT ingredients_problematiques, COUNT(*)
399
+ FROM arnaques
400
+ WHERE ingredients_problematiques IS NOT NULL
401
+ AND ingredients_problematiques != ''
402
+ GROUP BY ingredients_problematiques
403
+ ORDER BY COUNT(*) DESC
404
+ LIMIT 10
405
+ """)
406
+ stats['additifs_frequents'] = dict(cursor.fetchall())
407
+
408
+ conn.close()
409
+ return stats
410
+
411
+ def main():
412
+ """Fonction principale de l'application Streamlit"""
413
+
414
+ # Header principal
415
+ st.markdown("""
416
+ <div class="main-header">
417
+ <h1>🛡️ Foodwatch Arnaques Analyzer</h1>
418
+ <p>Scraping et analyse du Mur des Arnaques - Spécialisé Food Safety</p>
419
+ </div>
420
+ """, unsafe_allow_html=True)
421
+
422
+ # Initialisation de l'application
423
+ app = FoodwatchStreamlitApp()
424
+
425
+ # Sidebar pour la navigation
426
+ st.sidebar.title("🔧 Navigation")
427
+ page = st.sidebar.selectbox(
428
+ "Choisir une section",
429
+ ["🏠 Dashboard", "🕷️ Scraping", "📊 Analyses", "🔍 Données", "⚙️ Configuration"]
430
+ )
431
+
432
+ # PAGE DASHBOARD
433
+ if page == "🏠 Dashboard":
434
+ st.header("📈 Dashboard Principal")
435
+
436
+ # Chargement des données et statistiques
437
+ df = app.load_data_from_db()
438
+ stats = app.get_statistics()
439
+
440
+ if not df.empty:
441
+ # Métriques principales
442
+ col1, col2, col3, col4 = st.columns(4)
443
+
444
+ with col1:
445
+ st.metric(
446
+ label="🏷️ Total Produits",
447
+ value=stats['total_produits'],
448
+ delta="En base de données"
449
+ )
450
+
451
+ with col2:
452
+ st.metric(
453
+ label="🏪 Supermarchés",
454
+ value=len(stats['par_supermarche']),
455
+ delta="Chaînes concernées"
456
+ )
457
+
458
+ with col3:
459
+ st.metric(
460
+ label="🏭 Marques",
461
+ value=len(stats['par_marque']),
462
+ delta="Marques signalées"
463
+ )
464
+
465
+ with col4:
466
+ additifs_count = sum(1 for x in stats['additifs_frequents'].keys() if x.strip())
467
+ st.metric(
468
+ label="⚠️ Additifs",
469
+ value=additifs_count,
470
+ delta="Types détectés"
471
+ )
472
+
473
+ st.divider()
474
+
475
+ # Graphiques principaux
476
+ col1, col2 = st.columns(2)
477
+
478
+ with col1:
479
+ st.subheader("📊 Types d'Arnaques")
480
+ if stats['par_type']:
481
+ fig_pie = px.pie(
482
+ values=list(stats['par_type'].values()),
483
+ names=list(stats['par_type'].keys()),
484
+ color_discrete_sequence=px.colors.qualitative.Set3
485
+ )
486
+ fig_pie.update_traces(textposition='inside', textinfo='percent+label')
487
+ st.plotly_chart(fig_pie, use_container_width=True)
488
+
489
+ with col2:
490
+ st.subheader("🏪 Top Supermarchés")
491
+ if stats['par_supermarche']:
492
+ fig_bar = px.bar(
493
+ x=list(stats['par_supermarche'].values()),
494
+ y=list(stats['par_supermarche'].keys()),
495
+ orientation='h',
496
+ color=list(stats['par_supermarche'].values()),
497
+ color_continuous_scale="Reds"
498
+ )
499
+ fig_bar.update_layout(
500
+ xaxis_title="Nombre d'arnaques",
501
+ yaxis_title="Supermarchés"
502
+ )
503
+ st.plotly_chart(fig_bar, use_container_width=True)
504
+
505
+ # Évolution temporelle
506
+ st.subheader("📈 Évolution Temporelle")
507
+ df['date_signalement'] = pd.to_datetime(df['date_signalement'])
508
+ df_monthly = df.groupby(df['date_signalement'].dt.to_period('M')).size().reset_index()
509
+ df_monthly['date_signalement'] = df_monthly['date_signalement'].astype(str)
510
+
511
+ fig_line = px.line(
512
+ df_monthly,
513
+ x='date_signalement',
514
+ y=0,
515
+ title="Signalements par mois"
516
+ )
517
+ fig_line.update_layout(yaxis_title="Nombre de signalements")
518
+ st.plotly_chart(fig_line, use_container_width=True)
519
+
520
+ else:
521
+ st.info("💡 Aucune donnée disponible. Lancez un scraping dans la section '🕷️ Scraping'")
522
+
523
+ # PAGE SCRAPING
524
+ elif page == "🕷️ Scraping":
525
+ st.header("🕷️ Scraping du Mur des Arnaques")
526
+
527
+ col1, col2 = st.columns([2, 1])
528
+
529
+ with col1:
530
+ st.subheader("⚙️ Configuration du Scraping")
531
+
532
+ nb_pages = st.slider(
533
+ "Nombre de pages à scraper",
534
+ min_value=1, max_value=20, value=5,
535
+ help="Attention: plus de pages = plus de temps"
536
+ )
537
+
538
+ delay = st.slider(
539
+ "Délai entre requêtes (secondes)",
540
+ min_value=0.5, max_value=5.0, value=1.0, step=0.5,
541
+ help="Délai pour respecter les serveurs"
542
+ )
543
+
544
+ export_csv = st.checkbox(
545
+ "Export CSV automatique après scraping",
546
+ value=True
547
+ )
548
+
549
+ with col2:
550
+ st.subheader("ℹ️ Informations")
551
+ st.info("""
552
+ **Sources scrapées:**
553
+ - Mur des Arnaques Foodwatch
554
+ - Signalements citoyens
555
+ - Données validées par Foodwatch
556
+
557
+ **Données extraites:**
558
+ - Nom du produit
559
+ - Marque et supermarché
560
+ - Type d'arnaque
561
+ - Additifs problématiques
562
+ """)
563
+
564
+ st.divider()
565
+
566
+ # Bouton de lancement
567
+ col1, col2, col3 = st.columns([1, 2, 1])
568
+ with col2:
569
+ if st.button("🚀 Lancer le Scraping", type="primary", use_container_width=True):
570
+
571
+ st.subheader("📡 Scraping en cours...")
572
+
573
+ with st.spinner("Extraction des données..."):
574
+ # Simulation du scraping (remplacer par vrai scraping en production)
575
+ produits = app.simulate_scraping(nb_pages)
576
+
577
+ if produits:
578
+ st.success(f"✅ {len(produits)} produits extraits avec succès!")
579
+
580
+ # Sauvegarde en base
581
+ saved_count = app.save_to_database(produits)
582
+ st.info(f"💾 {saved_count} nouveaux produits sauvegardés en base")
583
+
584
+ # Aperçu des données
585
+ st.subheader("👀 Aperçu des données extraites")
586
+ df_preview = pd.DataFrame([asdict(p) for p in produits])
587
+ st.dataframe(df_preview[['nom_produit', 'marque', 'type_arnaque', 'ingredients_problematiques']])
588
+
589
+ # Export CSV si demandé
590
+ if export_csv:
591
+ csv_buffer = io.StringIO()
592
+ df_preview.to_csv(csv_buffer, index=False)
593
+ csv_data = csv_buffer.getvalue()
594
+
595
+ st.download_button(
596
+ label="📥 Télécharger CSV",
597
+ data=csv_data,
598
+ file_name=f"arnaques_foodwatch_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
599
+ mime="text/csv"
600
+ )
601
+ else:
602
+ st.error("❌ Aucune donnée extraite. Vérifiez la connexion.")
603
+
604
+ # PAGE ANALYSES
605
+ elif page == "📊 Analyses":
606
+ st.header("📊 Analyses Approfondies")
607
+
608
+ df = app.load_data_from_db()
609
+
610
+ if df.empty:
611
+ st.warning("⚠️ Aucune donnée disponible pour les analyses. Lancez d'abord un scraping.")
612
+ return
613
+
614
+ # Sélection du type d'analyse
615
+ analyse_type = st.selectbox(
616
+ "Type d'analyse",
617
+ ["🧪 Additifs Controversés", "🏭 Analyse par Marque", "🏪 Analyse par Supermarché", "📍 Analyse Géographique", "⏰ Tendances Temporelles"]
618
+ )
619
+
620
+ if analyse_type == "🧪 Additifs Controversés":
621
+ st.subheader("🧪 Analyse des Additifs Controversés")
622
+
623
+ # Filtre sur les produits avec additifs
624
+ df_additifs = df[df['ingredients_problematiques'].notna() & (df['ingredients_problematiques'] != '')]
625
+
626
+ if not df_additifs.empty:
627
+ col1, col2 = st.columns(2)
628
+
629
+ with col1:
630
+ # Comptage des additifs
631
+ additifs_list = []
632
+ for ingredients in df_additifs['ingredients_problematiques']:
633
+ additifs_list.extend([x.strip() for x in str(ingredients).split(',') if x.strip()])
634
+
635
+ additifs_count = pd.Series(additifs_list).value_counts()
636
+
637
+ fig_additifs = px.bar(
638
+ x=additifs_count.values,
639
+ y=additifs_count.index,
640
+ orientation='h',
641
+ title="Additifs les plus fréquents"
642
+ )
643
+ st.plotly_chart(fig_additifs, use_container_width=True)
644
+
645
+ with col2:
646
+ # Répartition par marque
647
+ marque_additifs = df_additifs.groupby('marque').size().sort_values(ascending=False).head(10)
648
+
649
+ fig_marques = px.pie(
650
+ values=marque_additifs.values,
651
+ names=marque_additifs.index,
652
+ title="Marques avec additifs problématiques"
653
+ )
654
+ st.plotly_chart(fig_marques, use_container_width=True)
655
+
656
+ # Table des additifs de référence
657
+ st.subheader("📚 Base de Référence des Additifs")
658
+ conn = sqlite3.connect(app.db_path)
659
+ df_ref = pd.read_sql_query("SELECT * FROM additifs_references", conn)
660
+ conn.close()
661
+
662
+ if not df_ref.empty:
663
+ st.dataframe(df_ref, use_container_width=True)
664
+ else:
665
+ st.info("Aucun produit avec additifs problématiques détecté.")
666
+
667
+ elif analyse_type == "🏭 Analyse par Marque":
668
+ st.subheader("🏭 Analyse par Marque")
669
+
670
+ # Top marques les plus signalées
671
+ marques_count = df['marque'].value_counts().head(15)
672
+
673
+ fig_marques = px.bar(
674
+ x=marques_count.index,
675
+ y=marques_count.values,
676
+ title="Top 15 des marques les plus signalées"
677
+ )
678
+ fig_marques.update_xaxes(tickangle=45)
679
+ st.plotly_chart(fig_marques, use_container_width=True)
680
+
681
+ # Analyse par type d'arnaque par marque
682
+ st.subheader("Types d'arnaques par marque")
683
+ marque_selected = st.selectbox("Sélectionner une marque", df['marque'].unique())
684
+
685
+ if marque_selected:
686
+ df_marque = df[df['marque'] == marque_selected]
687
+ types_count = df_marque['type_arnaque'].value_counts()
688
+
689
+ col1, col2 = st.columns(2)
690
+
691
+ with col1:
692
+ fig_types = px.pie(
693
+ values=types_count.values,
694
+ names=types_count.index,
695
+ title=f"Types d'arnaques - {marque_selected}"
696
+ )
697
+ st.plotly_chart(fig_types, use_container_width=True)
698
+
699
+ with col2:
700
+ st.write("**Détails des signalements:**")
701
+ st.dataframe(df_marque[['nom_produit', 'type_arnaque', 'description', 'date_signalement']])
702
+
703
+ elif analyse_type == "🏪 Analyse par Supermarché":
704
+ st.subheader("🏪 Analyse par Supermarché")
705
+
706
+ # Comparaison des supermarchés
707
+ supermarches_count = df['supermarche'].value_counts()
708
+
709
+ fig_super = px.bar(
710
+ x=supermarches_count.values,
711
+ y=supermarches_count.index,
712
+ orientation='h',
713
+ title="Signalements par supermarché",
714
+ color=supermarches_count.values,
715
+ color_continuous_scale="Reds"
716
+ )
717
+ st.plotly_chart(fig_super, use_container_width=True)
718
+
719
+ # Heatmap types d'arnaques vs supermarchés
720
+ st.subheader("Heatmap: Types d'arnaques par Supermarché")
721
+ heatmap_data = df.groupby(['supermarche', 'type_arnaque']).size().unstack(fill_value=0)
722
+
723
+ if not heatmap_data.empty:
724
+ fig_heatmap = px.imshow(
725
+ heatmap_data.values,
726
+ x=heatmap_data.columns,
727
+ y=heatmap_data.index,
728
+ aspect="auto",
729
+ color_continuous_scale="Reds",
730
+ title="Intensité des arnaques par type et supermarché"
731
+ )
732
+ fig_heatmap.update_xaxes(tickangle=45)
733
+ st.plotly_chart(fig_heatmap, use_container_width=True)
734
+
735
+ elif analyse_type == "📍 Analyse Géographique":
736
+ st.subheader("📍 Analyse Géographique")
737
+
738
+ # Répartition par ville
739
+ villes_count = df['ville'].value_counts().head(10)
740
+
741
+ col1, col2 = st.columns(2)
742
+
743
+ with col1:
744
+ fig_villes = px.bar(
745
+ x=villes_count.index,
746
+ y=villes_count.values,
747
+ title="Top 10 des villes avec le plus de signalements"
748
+ )
749
+ fig_villes.update_xaxes(tickangle=45)
750
+ st.plotly_chart(fig_villes, use_container_width=True)
751
+
752
+ with col2:
753
+ # Répartition par région (estimation basée sur les grandes villes)
754
+ regions_map = {
755
+ 'Paris': 'Île-de-France',
756
+ 'Lyon': 'Auvergne-Rhône-Alpes',
757
+ 'Marseille': 'Provence-Alpes-Côte d\'Azur',
758
+ 'Toulouse': 'Occitanie',
759
+ 'Bordeaux': 'Nouvelle-Aquitaine',
760
+ 'Nice': 'Provence-Alpes-Côte d\'Azur',
761
+ 'Nantes': 'Pays de la Loire',
762
+ 'Lille': 'Hauts-de-France'
763
+ }
764
+
765
+ df['region'] = df['ville'].map(regions_map).fillna('Autres')
766
+ regions_count = df['region'].value_counts()
767
+
768
+ fig_regions = px.pie(
769
+ values=regions_count.values,
770
+ names=regions_count.index,
771
+ title="Répartition par région"
772
+ )
773
+ st.plotly_chart(fig_regions, use_container_width=True)
774
+
775
+ elif analyse_type == "⏰ Tendances Temporelles":
776
+ st.subheader("⏰ Analyse des Tendances Temporelles")
777
+
778
+ df['date_signalement'] = pd.to_datetime(df['date_signalement'])
779
+
780
+ # Évolution par mois
781
+ df_monthly = df.groupby([df['date_signalement'].dt.to_period('M'), 'type_arnaque']).size().unstack(fill_value=0)
782
+ df_monthly.index = df_monthly.index.astype(str)
783
+
784
+ if not df_monthly.empty:
785
+ fig_evolution = go.Figure()
786
+
787
+ for col in df_monthly.columns:
788
+ fig_evolution.add_trace(go.Scatter(
789
+ x=df_monthly.index,
790
+ y=df_monthly[col],
791
+ mode='lines+markers',
792
+ name=col,
793
+ line=dict(width=3)
794
+ ))
795
+
796
+ fig_evolution.update_layout(
797
+ title="Évolution des types d'arnaques dans le temps",
798
+ xaxis_title="Mois",
799
+ yaxis_title="Nombre de signalements",
800
+ legend_title="Type d'arnaque"
801
+ )
802
+ st.plotly_chart(fig_evolution, use_container_width=True)
803
+
804
+ # Analyse saisonnière
805
+ df['mois'] = df['date_signalement'].dt.month
806
+ mois_count = df['mois'].value_counts().sort_index()
807
+ mois_noms = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun',
808
+ 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc']
809
+
810
+ fig_saison = px.bar(
811
+ x=[mois_noms[i-1] for i in mois_count.index],
812
+ y=mois_count.values,
813
+ title="Saisonnalité des signalements"
814
+ )
815
+ st.plotly_chart(fig_saison, use_container_width=True)
816
+
817
+ # PAGE DONNÉES
818
+ elif page == "🔍 Données":
819
+ st.header("🔍 Exploration des Données")
820
+
821
+ df = app.load_data_from_db()
822
+
823
+ if df.empty:
824
+ st.warning("⚠️ Aucune donnée disponible. Lancez d'abord un scraping.")
825
+ return
826
+
827
+ # Filtres
828
+ st.subheader("🔎 Filtres")
829
+ col1, col2, col3 = st.columns(3)
830
+
831
+ with col1:
832
+ marques_filter = st.multiselect(
833
+ "Filtrer par marque",
834
+ options=df['marque'].unique(),
835
+ default=[]
836
+ )
837
+
838
+ with col2:
839
+ types_filter = st.multiselect(
840
+ "Filtrer par type d'arnaque",
841
+ options=df['type_arnaque'].unique(),
842
+ default=[]
843
+ )
844
+
845
+ with col3:
846
+ supermaches_filter = st.multiselect(
847
+ "Filtrer par supermarché",
848
+ options=df['supermarche'].unique(),
849
+ default=[]
850
+ )
851
+
852
+ # Application des filtres
853
+ df_filtered = df.copy()
854
+
855
+ if marques_filter:
856
+ df_filtered = df_filtered[df_filtered['marque'].isin(marques_filter)]
857
+ if types_filter:
858
+ df_filtered = df_filtered[df_filtered['type_arnaque'].isin(types_filter)]
859
+ if supermaches_filter:
860
+ df_filtered = df_filtered[df_filtered['supermarche'].isin(supermaches_filter)]
861
+
862
+ # Recherche textuelle
863
+ search_term = st.text_input("🔍 Recherche textuelle dans les descriptions")
864
+ if search_term:
865
+ df_filtered = df_filtered[
866
+ df_filtered['description'].str.contains(search_term, case=False, na=False) |
867
+ df_filtered['nom_produit'].str.contains(search_term, case=False, na=False)
868
+ ]
869
+
870
+ st.divider()
871
+
872
+ # Affichage des résultats
873
+ st.subheader(f"📋 Résultats ({len(df_filtered)} produits)")
874
+
875
+ if not df_filtered.empty:
876
+ # Options d'affichage
877
+ col1, col2 = st.columns([3, 1])
878
+
879
+ with col1:
880
+ show_cols = st.multiselect(
881
+ "Colonnes à afficher",
882
+ options=['nom_produit', 'marque', 'supermarche', 'ville', 'type_arnaque',
883
+ 'description', 'prix', 'ingredients_problematiques', 'date_signalement'],
884
+ default=['nom_produit', 'marque', 'type_arnaque', 'ingredients_problematiques']
885
+ )
886
+
887
+ with col2:
888
+ export_filtered = st.button("📥 Exporter sélection", type="secondary")
889
+
890
+ # Tableau des données
891
+ if show_cols:
892
+ st.dataframe(
893
+ df_filtered[show_cols],
894
+ use_container_width=True,
895
+ height=400
896
+ )
897
+
898
+ # Export des données filtrées
899
+ if export_filtered:
900
+ csv_buffer = io.StringIO()
901
+ df_filtered.to_csv(csv_buffer, index=False)
902
+ csv_data = csv_buffer.getvalue()
903
+
904
+ st.download_button(
905
+ label="📥 Télécharger CSV filtré",
906
+ data=csv_data,
907
+ file_name=f"arnaques_foodwatch_filtered_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
908
+ mime="text/csv"
909
+ )
910
+
911
+ # Détails d'un produit
912
+ st.subheader("🔍 Détail d'un produit")
913
+ selected_product = st.selectbox(
914
+ "Sélectionner un produit pour voir les détails",
915
+ options=range(len(df_filtered)),
916
+ format_func=lambda x: df_filtered.iloc[x]['nom_produit']
917
+ )
918
+
919
+ if selected_product is not None:
920
+ product = df_filtered.iloc[selected_product]
921
+
922
+ col1, col2 = st.columns(2)
923
+
924
+ with col1:
925
+ st.write("**Informations Générales**")
926
+ st.write(f"**Produit:** {product['nom_produit']}")
927
+ st.write(f"**Marque:** {product['marque']}")
928
+ st.write(f"**Supermarché:** {product['supermarche']} ({product['ville']})")
929
+ st.write(f"**Prix:** {product['prix']}")
930
+ st.write(f"**Date signalement:** {product['date_signalement']}")
931
+
932
+ with col2:
933
+ st.write("**Analyse Food Safety**")
934
+ st.write(f"**Type d'arnaque:** {product['type_arnaque']}")
935
+
936
+ if product['ingredients_problematiques']:
937
+ st.warning(f"⚠️ **Additifs problématiques:** {product['ingredients_problematiques']}")
938
+ else:
939
+ st.success("✅ Aucun additif problématique détecté")
940
+
941
+ if product['description']:
942
+ st.write("**Description de l'arnaque:**")
943
+ st.write(product['description'])
944
+ else:
945
+ st.info("Aucun résultat ne correspond aux filtres sélectionnés.")
946
+
947
+ # PAGE CONFIGURATION
948
+ elif page == "⚙️ Configuration":
949
+ st.header("⚙️ Configuration de l'Application")
950
+
951
+ # Configuration de la base de données
952
+ st.subheader("🗄️ Base de Données")
953
+
954
+ col1, col2 = st.columns(2)
955
+
956
+ with col1:
957
+ if st.button("🔄 Réinitialiser la base de données", type="secondary"):
958
+ if st.button("⚠️ Confirmer la réinitialisation"):
959
+ try:
960
+ import os
961
+ if os.path.exists(app.db_path):
962
+ os.remove(app.db_path)
963
+ app.init_database()
964
+ st.success("✅ Base de données réinitialisée")
965
+ st.experimental_rerun()
966
+ except Exception as e:
967
+ st.error(f"❌ Erreur: {e}")
968
+
969
+ with col2:
970
+ if st.button("💾 Sauvegarder la base de données"):
971
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
972
+ backup_name = f"backup_foodwatch_{timestamp}.db"
973
+ try:
974
+ import shutil
975
+ shutil.copy2(app.db_path, backup_name)
976
+ st.success(f"✅ Sauvegarde créée: {backup_name}")
977
+ except Exception as e:
978
+ st.error(f"❌ Erreur: {e}")
979
+
980
+ st.divider()
981
+
982
+ # Configuration du scraping
983
+ st.subheader("🕷️ Configuration Scraping")
984
+
985
+ with st.expander("Paramètres avancés"):
986
+ base_url = st.text_input(
987
+ "URL de base Foodwatch",
988
+ value="https://www.foodwatch.org",
989
+ help="URL racine du site Foodwatch"
990
+ )
991
+
992
+ user_agent = st.text_input(
993
+ "User-Agent",
994
+ value="Mozilla/5.0 (compatible; FoodwatchAnalyzer/1.0)",
995
+ help="User-Agent pour les requêtes HTTP"
996
+ )
997
+
998
+ max_retries = st.number_input(
999
+ "Nombre max de tentatives",
1000
+ min_value=1, max_value=10, value=3,
1001
+ help="Nombre de tentatives en cas d'échec"
1002
+ )
1003
+
1004
+ timeout = st.number_input(
1005
+ "Timeout (secondes)",
1006
+ min_value=5, max_value=60, value=30,
1007
+ help="Timeout pour les requêtes HTTP"
1008
+ )
1009
+
1010
+ st.divider()
1011
+
1012
+ # Informations système
1013
+ st.subheader("ℹ️ Informations Système")
1014
+
1015
+ col1, col2 = st.columns(2)
1016
+
1017
+ with col1:
1018
+ st.write("**Base de données:**")
1019
+ if os.path.exists(app.db_path):
1020
+ file_size = os.path.getsize(app.db_path) / 1024 # KB
1021
+ st.write(f"- Taille: {file_size:.1f} KB")
1022
+ st.write(f"- Chemin: {app.db_path}")
1023
+
1024
+ # Statistiques de la base
1025
+ stats = app.get_statistics()
1026
+ st.write(f"- Total produits: {stats['total_produits']}")
1027
+ else:
1028
+ st.write("- Base non initialisée")
1029
+
1030
+ with col2:
1031
+ st.write("**Application:**")
1032
+ st.write("- Version: 1.0.0")
1033
+ st.write("- Framework: Streamlit")
1034
+ st.write("- Python:", sys.version.split()[0])
1035
+ st.write("- Date:", datetime.now().strftime("%Y-%m-%d %H:%M"))
1036
+
1037
+ st.divider()
1038
+
1039
+ # Export de configuration
1040
+ st.subheader("📁 Export/Import Configuration")
1041
+
1042
+ col1, col2 = st.columns(2)
1043
+
1044
+ with col1:
1045
+ if st.button("📤 Exporter configuration"):
1046
+ config = {
1047
+ "base_url": base_url,
1048
+ "user_agent": user_agent,
1049
+ "max_retries": max_retries,
1050
+ "timeout": timeout,
1051
+ "export_date": datetime.now().isoformat()
1052
+ }
1053
+
1054
+ config_json = json.dumps(config, indent=2)
1055
+ st.download_button(
1056
+ label="💾 Télécharger config.json",
1057
+ data=config_json,
1058
+ file_name="foodwatch_config.json",
1059
+ mime="application/json"
1060
+ )
1061
+
1062
+ with col2:
1063
+ uploaded_config = st.file_uploader(
1064
+ "📥 Importer configuration",
1065
+ type=['json'],
1066
+ help="Importer un fichier de configuration"
1067
+ )
1068
+
1069
+ if uploaded_config is not None:
1070
+ try:
1071
+ config = json.load(uploaded_config)
1072
+ st.success("✅ Configuration importée avec succès")
1073
+ st.json(config)
1074
+ except Exception as e:
1075
+ st.error(f"❌ Erreur lecture config: {e}")
1076
+
1077
+ # Footer
1078
+ st.divider()
1079
+ st.markdown("""
1080
+ <div style="text-align: center; color: #666; padding: 20px;">
1081
+ 🛡️ <strong>Foodwatch Arnaques Analyzer</strong> |
1082
+ Développé pour les professionnels de la food safety |
1083
+ <a href="https://www.foodwatch.org" target="_blank">Source: Foodwatch.org</a>
1084
+ </div>
1085
+ """, unsafe_allow_html=True)
1086
 
1087
+ # Point d'entrée principal
1088
+ if __name__ == "__main__":
1089
+ main()