-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconfig.py
More file actions
431 lines (397 loc) · 22.7 KB
/
Copy pathconfig.py
File metadata and controls
431 lines (397 loc) · 22.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
"""
Configuration de l'application ProspectLab
"""
import os
from pathlib import Path
# Charger les variables d'environnement depuis .env si disponible
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
# python-dotenv n'est pas installé, on continue sans
pass
# Chemins de base
BASE_DIR = Path(__file__).parent.parent
APP_DIR = Path(__file__).parent
# Configuration Flask
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
# Débogueur Werkzeug / pages d'erreur détaillées : désactivé par défaut (production).
# Mettre FLASK_DEBUG=1 uniquement en développement local — jamais sur Internet.
FLASK_DEBUG = os.environ.get('FLASK_DEBUG', '0').lower() in ('1', 'true', 'yes', 'on')
UPLOAD_FOLDER = Path(os.environ.get('UPLOAD_FOLDER', str(APP_DIR / 'uploads')))
EXPORT_FOLDER = Path(os.environ.get('EXPORT_FOLDER', str(APP_DIR / 'exports')))
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size
# Restriction optionnelle à un réseau local (similaire à MailPilot)
# Si RESTRICT_TO_LOCAL_NETWORK=true dans l'environnement, l'app ne sera
# accessible qu'au réseau local, sauf pour certaines routes publiques
# (tracking / API publique).
RESTRICT_TO_LOCAL_NETWORK = os.environ.get('RESTRICT_TO_LOCAL_NETWORK', 'false').lower() == 'true'
# Créer les dossiers si nécessaire
UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
EXPORT_FOLDER.mkdir(parents=True, exist_ok=True)
# Rapports PDF d'audit site (API publique)
AUDIT_REPORTS_DIR = Path(os.environ.get('AUDIT_REPORTS_DIR', str(APP_DIR / 'exports' / 'audit_reports')))
AUDIT_REPORTS_DIR.mkdir(parents=True, exist_ok=True)
WEBSITE_AUDIT_EMAIL_SUBJECT = os.environ.get(
'WEBSITE_AUDIT_EMAIL_SUBJECT',
'Votre rapport d\'audit web — {company}',
)
# Clé optionnelle pour formulaires lead (header X-Website-Audit-Key) sans token API Bearer
PUBLIC_WEBSITE_AUDIT_LEAD_KEY = (os.environ.get('PUBLIC_WEBSITE_AUDIT_LEAD_KEY') or '').strip()
def _audit_env(*keys: str, default: str = '') -> str:
for key in keys:
val = os.environ.get(key)
if val is not None and str(val).strip() != '':
return str(val).strip()
return default
# Rapport d'audit PDF via agent Cursor distant (infra SSH partagée avec landing variants)
WEBSITE_AUDIT_AGENT_ENABLED = _audit_env('WEBSITE_AUDIT_AGENT_ENABLED', 'WEBSITE_AUDIT_SERV1_ENABLED', default='true').lower() in (
'1', 'true', 'yes', 'on',
)
WEBSITE_AUDIT_AGENT_SCRIPT_PATH = _audit_env(
'WEBSITE_AUDIT_AGENT_SCRIPT_PATH',
'WEBSITE_AUDIT_SERV1_SCRIPT_PATH',
default=str(APP_DIR / 'scripts' / 'experiments' / 'gen_audit_report' / 'generate_website_audit_cursor_remote.py'),
)
WEBSITE_AUDIT_AGENT_TIMEOUT = int(_audit_env('WEBSITE_AUDIT_AGENT_TIMEOUT', 'WEBSITE_AUDIT_SERV1_AGENT_TIMEOUT', default='1500') or '1500')
WEBSITE_AUDIT_AGENT_REMOTE_TEMP_ROOT = _audit_env(
'WEBSITE_AUDIT_AGENT_REMOTE_TEMP_ROOT',
'WEBSITE_AUDIT_SERV1_REMOTE_TEMP_ROOT',
default=_audit_env('LANDING_VARIANTS_REMOTE_TEMP_ROOT', default='C:\\Temp\\cursor_prompt_runner'),
)
WEBSITE_AUDIT_AGENT_REMOTE_OUTPUT_ROOT = _audit_env(
'WEBSITE_AUDIT_AGENT_REMOTE_OUTPUT_ROOT',
'WEBSITE_AUDIT_SERV1_REMOTE_OUTPUT_ROOT',
default='C:\\Temp\\cursor_generated_audit_reports',
)
# Alias rétrocompatibilité
WEBSITE_AUDIT_SERV1_ENABLED = WEBSITE_AUDIT_AGENT_ENABLED
WEBSITE_AUDIT_SERV1_SCRIPT_PATH = WEBSITE_AUDIT_AGENT_SCRIPT_PATH
WEBSITE_AUDIT_SERV1_AGENT_TIMEOUT = WEBSITE_AUDIT_AGENT_TIMEOUT
WEBSITE_AUDIT_SERV1_REMOTE_TEMP_ROOT = WEBSITE_AUDIT_AGENT_REMOTE_TEMP_ROOT
WEBSITE_AUDIT_SERV1_REMOTE_OUTPUT_ROOT = WEBSITE_AUDIT_AGENT_REMOTE_OUTPUT_ROOT
# Rapport audit : timeouts par module (secondes, 0 = illimité) et poursuite si échec
WEBSITE_AUDIT_SOFT_FAIL_MODULES = os.environ.get('WEBSITE_AUDIT_SOFT_FAIL_MODULES', 'true').lower() in (
'1', 'true', 'yes', 'on',
)
WEBSITE_AUDIT_TECHNICAL_TIMEOUT_SEC = max(0, int(os.environ.get('WEBSITE_AUDIT_TECHNICAL_TIMEOUT_SEC', '600')))
WEBSITE_AUDIT_SEO_TIMEOUT_SEC = max(0, int(os.environ.get('WEBSITE_AUDIT_SEO_TIMEOUT_SEC', '900')))
WEBSITE_AUDIT_PENTEST_TIMEOUT_SEC = max(0, int(os.environ.get('WEBSITE_AUDIT_PENTEST_TIMEOUT_SEC', '1200')))
WEBSITE_AUDIT_SCRAPING_TIMEOUT_SEC = max(0, int(os.environ.get('WEBSITE_AUDIT_SCRAPING_TIMEOUT_SEC', '600')))
# Pause + email de reprise si l'agent PDF échoue (pas de PDF local envoyé au client)
WEBSITE_AUDIT_AGENT_PAUSE_ON_AGENT_FAILURE = _audit_env(
'WEBSITE_AUDIT_AGENT_PAUSE_ON_AGENT_FAILURE',
'WEBSITE_AUDIT_AGENT_PAUSE_ON_USAGE_LIMIT',
'WEBSITE_AUDIT_SERV1_PAUSE_ON_USAGE_LIMIT',
default='true',
).lower() in ('1', 'true', 'yes', 'on')
WEBSITE_AUDIT_AGENT_FALLBACK_LOCAL = _audit_env(
'WEBSITE_AUDIT_AGENT_FALLBACK_LOCAL', 'WEBSITE_AUDIT_SERV1_FALLBACK_LOCAL', default='false'
).lower() in ('1', 'true', 'yes', 'on')
WEBSITE_AUDIT_AGENT_PAUSE_ON_USAGE_LIMIT = WEBSITE_AUDIT_AGENT_PAUSE_ON_AGENT_FAILURE
WEBSITE_AUDIT_SERV1_PAUSE_ON_USAGE_LIMIT = WEBSITE_AUDIT_AGENT_PAUSE_ON_USAGE_LIMIT
WEBSITE_AUDIT_SERV1_FALLBACK_LOCAL = WEBSITE_AUDIT_AGENT_FALLBACK_LOCAL
WEBSITE_AUDIT_CURSOR_ALERT_EMAIL = (
os.environ.get('WEBSITE_AUDIT_CURSOR_ALERT_EMAIL') or os.environ.get('MAIL_DEFAULT_RECIPIENT') or ''
).strip()
WEBSITE_AUDIT_USAGE_LIMIT_RETRIES = max(0, int(os.environ.get('WEBSITE_AUDIT_USAGE_LIMIT_RETRIES', '1')))
WEBSITE_AUDIT_USAGE_LIMIT_RETRY_DELAY_SEC = max(
60, int(os.environ.get('WEBSITE_AUDIT_USAGE_LIMIT_RETRY_DELAY_SEC', '300'))
)
# Configuration email (à configurer selon ton serveur)
MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.gmail.com')
MAIL_PORT = int(os.environ.get('MAIL_PORT', 587))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'True').lower() == 'true'
MAIL_USERNAME = os.environ.get('MAIL_USERNAME', '')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD', '')
MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER', 'Loic Daniel <loic@example.com>')
# Destinataire par défaut pour les rapports internes (campagnes, diagnostics, etc.)
MAIL_DEFAULT_RECIPIENT = os.environ.get('MAIL_DEFAULT_RECIPIENT', 'contact@danielcraft.fr')
# Configuration scraping
SCRAPING_DELAY = 2.0 # Délai entre requêtes (secondes)
SCRAPING_MAX_WORKERS = 3 # Nombre de threads parallèles
SCRAPING_MAX_DEPTH = 3 # Profondeur maximale de scraping
WEBSITE_SCREENSHOTS_DIR = Path(
os.environ.get('WEBSITE_SCREENSHOTS_DIR', str(APP_DIR / 'static' / 'screenshots' / 'website_previews'))
)
WEBSITE_SCREENSHOTS_BASE_URL = (
os.environ.get('WEBSITE_SCREENSHOTS_BASE_URL', '/static/screenshots/website_previews').strip()
or '/static/screenshots/website_previews'
)
# Après chargement : courte pause pour fonts/animations (vitesse vs rendu).
WEBSITE_SCREENSHOT_WAIT_MS = int(os.environ.get('WEBSITE_SCREENSHOT_WAIT_MS', '700'))
# domcontentloaded = plus rapide que networkidle ; load = un peu plus lourd.
WEBSITE_SCREENSHOT_GOTO_WAIT_UNTIL = (
os.environ.get('WEBSITE_SCREENSHOT_GOTO_WAIT_UNTIL', 'domcontentloaded').strip().lower()
or 'domcontentloaded'
)
WEBSITE_SCREENSHOT_WEBP_QUALITY = int(os.environ.get('WEBSITE_SCREENSHOT_WEBP_QUALITY', '78'))
# Viewports de capture (pas full-page, mais plus "longs" pour couvrir plus que le hero).
WEBSITE_SCREENSHOT_VIEWPORT_DESKTOP_WIDTH = max(900, int(os.environ.get('WEBSITE_SCREENSHOT_VIEWPORT_DESKTOP_WIDTH', '1920')))
WEBSITE_SCREENSHOT_VIEWPORT_DESKTOP_HEIGHT = max(700, int(os.environ.get('WEBSITE_SCREENSHOT_VIEWPORT_DESKTOP_HEIGHT', '1600')))
WEBSITE_SCREENSHOT_VIEWPORT_TABLET_WIDTH = max(700, int(os.environ.get('WEBSITE_SCREENSHOT_VIEWPORT_TABLET_WIDTH', '1024')))
WEBSITE_SCREENSHOT_VIEWPORT_TABLET_HEIGHT = max(900, int(os.environ.get('WEBSITE_SCREENSHOT_VIEWPORT_TABLET_HEIGHT', '1700')))
WEBSITE_SCREENSHOT_VIEWPORT_MOBILE_WIDTH = max(300, int(os.environ.get('WEBSITE_SCREENSHOT_VIEWPORT_MOBILE_WIDTH', '430')))
WEBSITE_SCREENSHOT_VIEWPORT_MOBILE_HEIGHT = max(650, int(os.environ.get('WEBSITE_SCREENSHOT_VIEWPORT_MOBILE_HEIGHT', '1200')))
# Largeur max avant WEBP (réduit poids disque ; None pour désactiver).
WEBSITE_SCREENSHOT_MAX_WIDTH_DESKTOP = int(os.environ.get('WEBSITE_SCREENSHOT_MAX_WIDTH_DESKTOP', '1600'))
WEBSITE_SCREENSHOT_MAX_WIDTH_TABLET = int(os.environ.get('WEBSITE_SCREENSHOT_MAX_WIDTH_TABLET', '1200'))
WEBSITE_SCREENSHOT_MAX_WIDTH_MOBILE = int(os.environ.get('WEBSITE_SCREENSHOT_MAX_WIDTH_MOBILE', '800'))
WEBSITE_SCREENSHOT_KEEP_SETS = max(1, int(os.environ.get('WEBSITE_SCREENSHOT_KEEP_SETS', '5')))
WEBSITE_SCREENSHOT_CLEANUP_BATCH_SIZE = max(
10,
min(5000, int(os.environ.get('WEBSITE_SCREENSHOT_CLEANUP_BATCH_SIZE', '1500'))),
)
# True = les 3 viewports en parallèle (async Playwright) ; False = séquentiel (moins de RAM).
WEBSITE_SCREENSHOT_PARALLEL = os.environ.get('WEBSITE_SCREENSHOT_PARALLEL', 'true').lower() in (
'1',
'true',
'yes',
'on',
)
# Timeout navigation (ms) pour page.goto.
WEBSITE_SCREENSHOT_GOTO_TIMEOUT_MS = int(os.environ.get('WEBSITE_SCREENSHOT_GOTO_TIMEOUT_MS', '28000'))
# Capture navigateur : jpeg = moins de données que png avant encodage WEBP.
_fmt = os.environ.get('WEBSITE_SCREENSHOT_CAPTURE_FORMAT', 'jpeg').strip().lower() or 'jpeg'
WEBSITE_SCREENSHOT_CAPTURE_FORMAT = _fmt if _fmt in ('jpeg', 'jpg', 'png') else 'jpeg'
WEBSITE_SCREENSHOT_JPEG_QUALITY = max(50, min(95, int(os.environ.get('WEBSITE_SCREENSHOT_JPEG_QUALITY', '82'))))
# 1 = moins de pixels (fichiers plus légers) ; 2 = plus net sur viewports « mobile ».
WEBSITE_SCREENSHOT_DEVICE_SCALE_FACTOR = max(
1.0,
min(3.0, float(os.environ.get('WEBSITE_SCREENSHOT_DEVICE_SCALE_FACTOR', '1'))),
)
WEBSITE_SCREENSHOT_BLOCK_TRACKERS = os.environ.get('WEBSITE_SCREENSHOT_BLOCK_TRACKERS', 'true').lower() in (
'1',
'true',
'yes',
'on',
)
WEBSITE_SCREENSHOT_REDUCED_MOTION = os.environ.get('WEBSITE_SCREENSHOT_REDUCED_MOTION', 'true').lower() in (
'1',
'true',
'yes',
'on',
)
WEBSITE_SCREENSHOT_DISABLE_ANIMATIONS = os.environ.get('WEBSITE_SCREENSHOT_DISABLE_ANIMATIONS', 'true').lower() in (
'1',
'true',
'yes',
'on',
)
# Recadrage automatique du blanc en bas (utile sur sites très "courts" dans de grands viewports).
WEBSITE_SCREENSHOT_TRIM_WHITE_BOTTOM = os.environ.get('WEBSITE_SCREENSHOT_TRIM_WHITE_BOTTOM', 'true').lower() in (
'1',
'true',
'yes',
'on',
)
WEBSITE_SCREENSHOT_TRIM_WHITE_THRESHOLD = max(
220,
min(254, int(os.environ.get('WEBSITE_SCREENSHOT_TRIM_WHITE_THRESHOLD', '245'))),
)
WEBSITE_SCREENSHOT_TRIM_WHITE_MIN_TRAILING_RATIO = max(
0.05,
min(0.9, float(os.environ.get('WEBSITE_SCREENSHOT_TRIM_WHITE_MIN_TRAILING_RATIO', '0.18'))),
)
WEBSITE_SCREENSHOT_TRIM_WHITE_PADDING_PX = max(
0,
min(300, int(os.environ.get('WEBSITE_SCREENSHOT_TRIM_WHITE_PADDING_PX', '24'))),
)
WEBSITE_SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True)
# Allowed extensions
ALLOWED_EXTENSIONS = {'xlsx', 'xls'}
# Configuration base de données
APP_ENV = (os.environ.get('APP_ENV') or 'development').strip().lower()
IS_PRODUCTION = APP_ENV in ('production', 'prod')
DATABASE_URL = (os.environ.get('DATABASE_URL') or '').strip()
DATABASE_PATH = os.environ.get('DATABASE_PATH', None) # None = chemin par défaut
# Pool PostgreSQL (activé en prod via APP_ENV=production ou DATABASE_POOL_ENABLED=true)
DATABASE_POOL_MIN = max(1, int(os.environ.get('DATABASE_POOL_MIN', '2') or '2'))
DATABASE_POOL_MAX = max(DATABASE_POOL_MIN, int(os.environ.get('DATABASE_POOL_MAX', '20') or '20'))
DATABASE_CONNECT_TIMEOUT_SEC = max(3, int(os.environ.get('DATABASE_CONNECT_TIMEOUT_SEC', '10') or '10'))
DATABASE_STATEMENT_TIMEOUT_MS = int(os.environ.get('DATABASE_STATEMENT_TIMEOUT_MS', '0') or '0')
# Configuration API Sirene (data.gouv.fr)
# L'API publique ne nécessite pas de clé, mais une clé permet plus de requêtes
SIRENE_API_KEY = os.environ.get('SIRENE_API_KEY', '')
SIRENE_API_URL = os.environ.get('SIRENE_API_URL', 'https://recherche-entreprises.api.gouv.fr/search')
# Configuration APIs OSINT (optionnelles mais recommandées)
SHODAN_API_KEY = os.environ.get('SHODAN_API_KEY', '')
CENSYS_API_ID = os.environ.get('CENSYS_API_ID', '')
CENSYS_API_SECRET = os.environ.get('CENSYS_API_SECRET', '')
HUNTER_API_KEY = os.environ.get('HUNTER_API_KEY', '')
# Abstract (https://www.abstractapi.com/) — validation email / téléphone en option
ABSTRACT_EMAIL_API_KEY = os.environ.get('ABSTRACT_EMAIL_API_KEY', '')
ABSTRACT_PHONE_API_KEY = os.environ.get('ABSTRACT_PHONE_API_KEY', '')
# Numverify (APILayer) — ligne type / pays / opérateur pour un E.164
NUMVERIFY_API_KEY = os.environ.get('NUMVERIFY_API_KEY', '')
BUILTWITH_API_KEY = os.environ.get('BUILTWITH_API_KEY', '')
HIBP_API_KEY = os.environ.get('HIBP_API_KEY', '')
# Configuration WSL (pour les outils OSINT/Pentest)
WSL_DISTRO = os.environ.get('WSL_DISTRO', 'kali-linux')
WSL_USER = os.environ.get('WSL_USER', 'loupix')
# Configuration timeout pour les outils externes
OSINT_TOOL_TIMEOUT = int(os.environ.get('OSINT_TOOL_TIMEOUT', '60')) # secondes
# Parse des numéros (lib phonenumbers) et cap analyse OSINT téléphone
PHONE_DEFAULT_REGION = os.environ.get('PHONE_DEFAULT_REGION', 'FR')
PHONE_OSINT_MAX_NUMBERS = int(os.environ.get('PHONE_OSINT_MAX_NUMBERS', '8'))
# PhoneInfoga v2 : scanners optionnels via variables documentées upstream (ex. NUMVERIFY_API_KEY).
PENTEST_TOOL_TIMEOUT = int(os.environ.get('PENTEST_TOOL_TIMEOUT', '120')) # secondes
# Analyse SEO : timeouts séparés connexion / lecture (plusieurs URL candidates)
SEO_FETCH_CONNECT_TIMEOUT = float(os.environ.get('SEO_FETCH_CONNECT_TIMEOUT', '12'))
SEO_FETCH_READ_TIMEOUT = float(os.environ.get('SEO_FETCH_READ_TIMEOUT', '25'))
SEO_TOOL_TIMEOUT = int(os.environ.get('SEO_TOOL_TIMEOUT', '120'))
# Active Lighthouse par défaut via l'environnement (prod recommandé).
SEO_USE_LIGHTHOUSE_DEFAULT = os.environ.get('SEO_USE_LIGHTHOUSE_DEFAULT', 'false').lower() in ('1', 'true', 'yes', 'on')
# HTTP 429 / 503 : nouveaux essais avec attente (Retry-After ou backoff exponentiel)
SEO_FETCH_RATE_LIMIT_MAX_RETRIES = max(
0, int(os.environ.get('SEO_FETCH_RATE_LIMIT_MAX_RETRIES', '5'))
)
SEO_FETCH_RATE_LIMIT_BASE_DELAY_SEC = float(
os.environ.get('SEO_FETCH_RATE_LIMIT_BASE_DELAY_SEC', '4')
)
# Pack « analyse site complète » : pause après le scraping avant technique/SEO (réduit le rafale sur l’hôte)
FULL_ANALYSIS_INTER_STEP_PAUSE_SEC = float(
os.environ.get('FULL_ANALYSIS_INTER_STEP_PAUSE_SEC', '3')
)
# Lighthouse (Node chrome-launcher) : sur Linux embarqué / Raspberry Pi, Chrome n’est pas dans le PATH standard
CHROME_PATH = (os.environ.get('CHROME_PATH') or os.environ.get('LIGHTHOUSE_CHROME_PATH') or '').strip() or None
# Configuration des limites de requêtes API
SIRENE_API_RATE_LIMIT = int(os.environ.get('SIRENE_API_RATE_LIMIT', '10')) # requêtes par minute
# Configuration Celery
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
# Redis pour que les workers Celery puissent émettre vers Socket.IO (voir Flask-SocketIO « external process »).
# Par défaut : même URL que le broker. Désactiver : SOCKETIO_DISABLE_MESSAGE_QUEUE=1
SOCKETIO_MESSAGE_QUEUE = (os.environ.get('SOCKETIO_MESSAGE_QUEUE') or '').strip() or CELERY_BROKER_URL
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TIMEZONE = 'Europe/Paris'
CELERY_ENABLE_UTC = True
CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes max par tâche
CELERY_TASK_SOFT_TIME_LIMIT = 25 * 60 # 25 minutes avant arrêt doux
# Nombre de workers Celery (4 en dev, 6 en prod par défaut)
CELERY_WORKERS = int(os.environ.get('CELERY_WORKERS', '4'))
# Étalement des sous-tâches lancées en masse (scraping multi-sites, etc.) : countdown = index * valeur
CELERY_BULK_STAGGER_SEC = float(os.environ.get('CELERY_BULK_STAGGER_SEC', '0.75'))
# Index Redis global (WebSocket) pris modulo cette valeur pour éviter un countdown énorme
# si la clé prospectlab:heavy:stagger:seq a été incrémentée des milliers de fois (sinon les
# tâches restent « planifiées » pendant des heures et le worker semble ne rien faire).
CELERY_BULK_STAGGER_SLOT_MODULO = max(1, int(os.environ.get('CELERY_BULK_STAGGER_SLOT_MODULO', '400')))
# 1 = le worker ne précharge qu'une tâche à la fois (meilleure répartition sous charge)
CELERY_WORKER_PREFETCH_MULTIPLIER = int(os.environ.get('CELERY_WORKER_PREFETCH_MULTIPLIER', '1'))
CELERY_TASK_ACKS_LATE = os.environ.get('CELERY_TASK_ACKS_LATE', 'true').lower() in ('1', 'true', 'yes')
# Files Celery à consommer.
# En dev, si CELERY_WORKER_QUEUES n'est pas fourni, on doit écouter aussi les queues dédiées
# (scraping/technical/seo/osint/pentest), sinon les tâches routées ne sont jamais exécutées.
CELERY_WORKER_QUEUES = os.environ.get(
'CELERY_WORKER_QUEUES',
'celery,scraping,scraping_interactive,mini_scrape,technical,seo,screenshot,osint,pentest,heavy,website_full,landing',
)
# Landing variants (Cursor distant / serv1)
LANDING_VARIANTS_ENABLED = os.environ.get('LANDING_VARIANTS_ENABLED', 'true').lower() in (
'1',
'true',
'yes',
'on',
)
LANDING_VARIANTS_SCRIPT_PATH = os.environ.get(
'LANDING_VARIANTS_SCRIPT_PATH',
str(APP_DIR / 'scripts' / 'experiments' / 'gen_new_website' / 'generate_landing_variants_cursor_remote.py'),
)
LANDING_VARIANTS_REMOTE_HOST = (
os.environ.get('LANDING_VARIANTS_REMOTE_HOST', 'loicDaniel@serv1.lan') or 'loicDaniel@serv1.lan'
).strip().strip('\r')
LANDING_VARIANTS_REMOTE_WORKSPACE = os.environ.get('LANDING_VARIANTS_REMOTE_WORKSPACE', '')
LANDING_VARIANTS_REMOTE_CURSOR_COMMAND = os.environ.get('LANDING_VARIANTS_REMOTE_CURSOR_COMMAND', 'agent.cmd')
LANDING_VARIANTS_MODEL = os.environ.get('LANDING_VARIANTS_MODEL', 'auto')
LANDING_VARIANTS_REMOTE_TEMP_ROOT = os.environ.get('LANDING_VARIANTS_REMOTE_TEMP_ROOT', 'C:\\Temp\\cursor_prompt_runner')
LANDING_VARIANTS_REMOTE_OUTPUT_ROOT = os.environ.get('LANDING_VARIANTS_REMOTE_OUTPUT_ROOT', 'C:\\Temp\\cursor_generated_landings')
LANDING_VARIANTS_SSH_KEY_PATH = (os.environ.get('LANDING_VARIANTS_SSH_KEY_PATH', '') or '').strip().strip('\r')
LANDING_VARIANTS_AGENT_TIMEOUT = int(os.environ.get('LANDING_VARIANTS_AGENT_TIMEOUT', '420'))
LANDING_VARIANTS_SERV1_MAX_CONCURRENT = max(
1, int(os.environ.get('LANDING_VARIANTS_SERV1_MAX_CONCURRENT', '1'))
)
LANDING_VARIANTS_SERV1_SEMAPHORE_KEY = (
os.environ.get('LANDING_VARIANTS_SERV1_SEMAPHORE_KEY', 'prospectlab:landing:serv1:inflight').strip()
or 'prospectlab:landing:serv1:inflight'
)
LANDING_VARIANTS_SERV1_SEMAPHORE_TTL_SEC = max(
60, int(os.environ.get('LANDING_VARIANTS_SERV1_SEMAPHORE_TTL_SEC', '7200'))
)
LANDING_VARIANTS_SERV1_WAIT_RETRY_SEC = max(
2, int(os.environ.get('LANDING_VARIANTS_SERV1_WAIT_RETRY_SEC', '15'))
)
LANDING_VARIANTS_SERV1_WAIT_MAX_SEC = max(
30, int(os.environ.get('LANDING_VARIANTS_SERV1_WAIT_MAX_SEC', '3600'))
)
LANDING_VARIANTS_USAGE_LIMIT_RETRIES = max(
0, int(os.environ.get('LANDING_VARIANTS_USAGE_LIMIT_RETRIES', '2'))
)
LANDING_VARIANTS_USAGE_LIMIT_RETRY_DELAY_SEC = max(
30, int(os.environ.get('LANDING_VARIANTS_USAGE_LIMIT_RETRY_DELAY_SEC', '900'))
)
LANDING_VARIANTS_STATIC_OUTPUT_DIR = os.environ.get(
'LANDING_VARIANTS_STATIC_OUTPUT_DIR',
str(APP_DIR / 'static' / 'generated' / 'landing_variants'),
)
LANDING_VARIANTS_PUBLIC_BASE_URL = (
os.environ.get('LANDING_VARIANTS_PUBLIC_BASE_URL', '/static/generated/landing_variants').strip()
or '/static/generated/landing_variants'
)
# File d’enqueue pour le pack « analyse site complet ».
# Défaut « technical » : les workers existants écoutent déjà cette file (voir CELERY_WORKER_QUEUES).
# Pour isoler le pack sur un worker dédié : CELERY_FULL_ANALYSIS_QUEUE=website_full et ajoutez
# « website_full » à CELERY_WORKER_QUEUES sur ce nœud (sinon la tâche reste PENDING à l’infini).
CELERY_FULL_ANALYSIS_QUEUE = (
(os.environ.get('CELERY_FULL_ANALYSIS_QUEUE') or 'technical').strip() or 'technical'
)
# Mise à l’échelle (cluster) : plusieurs workers → même CELERY_BROKER_URL vers Redis (ex. node15.lan).
# Pas de variable « nombre de workers » côté app : chaque nœud définit CELERY_WORKERS localement.
# Surveiller Redis (mémoire) et PostgreSQL max_connections — voir docs/configuration/DEPLOIEMENT_PRODUCTION.md.
def _resolve_public_base_url() -> str:
"""
URL publique pour liens email / API (tracking, reprise audit agent).
Si BASE_URL est déclaré plusieurs fois dans .env, python-dotenv ne garde que la
première entrée : on relit le fichier pour prendre la dernière valeur non commentée.
Priorité : WEBSITE_AUDIT_PUBLIC_BASE_URL > dernière BASE_URL dans .env > os.environ.
"""
for key in ('WEBSITE_AUDIT_PUBLIC_BASE_URL', 'WEBSITE_AUDIT_RESUME_BASE_URL'):
val = (os.environ.get(key) or '').strip()
if val:
return val.rstrip('/')
env_file = APP_DIR / '.env'
last_from_file = ''
if env_file.is_file():
for raw in env_file.read_text(encoding='utf-8', errors='replace').splitlines():
line = raw.strip()
if not line or line.startswith('#'):
continue
if line.startswith('export '):
line = line[7:].strip()
if line.startswith('BASE_URL='):
last_from_file = line.split('=', 1)[1].strip().strip('"').strip("'")
if last_from_file:
return last_from_file.rstrip('/')
return (os.environ.get('BASE_URL') or 'http://localhost:5000').strip().rstrip('/')
# URL de base pour le tracking des emails et liens publics (doit être accessible publiquement)
BASE_URL = _resolve_public_base_url()
# Scan automatique des bounces (IMAP -> tags/statuts)
BOUNCE_SCAN_ENABLED = os.environ.get('BOUNCE_SCAN_ENABLED', 'true').lower() in ('1', 'true', 'yes', 'on')
# Profils IMAP à scanner (doit correspondre à IMAP_PROFILES et variables IMAP_*_PROFIL)
BOUNCE_SCAN_PROFILES = (os.environ.get('BOUNCE_SCAN_PROFILES') or os.environ.get('IMAP_PROFILES') or 'default').strip()
# Fenêtre de scan périodique en jours (cron 2x/jour)
BOUNCE_SCAN_DAYS = max(1, int(os.environ.get('BOUNCE_SCAN_DAYS', '14')))
# Fenêtre de scan pour le run déclenché peu après lancement d'une campagne
BOUNCE_SCAN_AFTER_CAMPAIGN_DAYS = max(1, int(os.environ.get('BOUNCE_SCAN_AFTER_CAMPAIGN_DAYS', '2')))
# 0 = sans limite de messages IMAP, >0 = limite
BOUNCE_SCAN_LIMIT = int(os.environ.get('BOUNCE_SCAN_LIMIT', '0'))
# true = déplacer/supprimer les bounces traités côté IMAP (Gmail/node12)
BOUNCE_SCAN_DELETE_PROCESSED = os.environ.get('BOUNCE_SCAN_DELETE_PROCESSED', 'true').lower() in ('1', 'true', 'yes', 'on')
# Délai (secondes) après lancement campagne avant 1er scan auto
BOUNCE_SCAN_POST_CAMPAIGN_DELAY_SEC = max(30, int(os.environ.get('BOUNCE_SCAN_POST_CAMPAIGN_DELAY_SEC', '1800')))
# Cache mémoire des réponses GET /api/public (décorateur public_response_cache).
# PUBLIC_API_RESPONSE_CACHE=false pour désactiver.
# TTL par défaut (secondes) si un décorateur ne fixe pas de durée : PUBLIC_API_CACHE_TTL_DEFAULT=30
# Taille max du cache (entrées) : PUBLIC_API_CACHE_MAX_ENTRIES=512