Docfile commited on
Commit
e1feafa
·
verified ·
1 Parent(s): 56cfb68

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +275 -127
app.py CHANGED
@@ -1,29 +1,40 @@
1
- import Flask
2
- from flask import request, render_template_string, jsonify, redirect, url_for
 
 
3
  import requests
4
  import threading
5
- import uuid # Pour générer des identifiants uniques pour chaque tâche
6
  import time
7
- import copy # Pour copier le payload pour chaque requête
8
-
9
- app = Flask(__name__)
10
 
11
  # --- Configuration ---
12
- TARGET_URL = "https://hook.us1.make.com/zal5qn0ggbewmvtsbo2uenfno8tz3n56"
13
- BASE_PAYLOAD = {
14
  "name": "Testeur Auto ",
15
- "email": "[email protected]", # Ajout de '+auto' pour distinguer
16
  "company": "aragon Inc.",
17
  "message": "Ceci est un test automatisé via Flask.",
18
- "date": "2023-10-27T10:30:00Z", # Tu pourrais rendre cette date dynamique si besoin
19
  "source": "http://simulateur-bsbs-flask.com"
20
  }
 
 
 
 
 
 
 
 
21
  # Structure pour stocker l'état des tâches (jobs) en mémoire
22
- # Format: { 'job_id': {'status': 'running'/'completed'/'failed', 'total': N, 'completed_count': M, 'error_count': E, 'errors': [...] } }
23
- jobs = {}
24
- jobs_lock = threading.Lock() # Pour éviter les problèmes d'accès concurrents au dict jobs
 
 
25
 
26
- # --- Templates HTML ---
27
 
28
  # Page d'accueil pour démarrer les requêtes
29
  HTML_INDEX = """
@@ -34,30 +45,39 @@ HTML_INDEX = """
34
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
35
  <title>Lanceur de Requêtes</title>
36
  <style>
37
- body { font-family: sans-serif; margin: 20px; }
38
- label { display: block; margin-bottom: 5px; }
39
- input[type=number] { width: 100px; padding: 8px; margin-bottom: 15px; }
40
- button { padding: 10px 15px; cursor: pointer; }
41
- .error { color: red; margin-top: 10px; }
 
 
 
 
 
 
 
42
  </style>
43
  </head>
44
  <body>
45
  <h1>Envoyer des Requêtes POST en Masse</h1>
46
- <form method="POST" action="/start">
47
  <label for="num_requests">Nombre de requêtes à envoyer :</label>
48
- <input type="number" id="num_requests" name="num_requests" min="1" required>
49
  <button type="submit">Lancer les requêtes</button>
50
  </form>
51
  {% if error %}
52
  <p class="error">{{ error }}</p>
53
  {% endif %}
54
 
55
- <h2>Tâches en cours / terminées :</h2>
56
  <ul>
57
  {% for job_id, job_info in jobs_list.items() %}
58
  <li>
59
  <a href="{{ url_for('job_status', job_id=job_id) }}">Tâche {{ job_id }}</a>
60
- ({{ job_info.status }}, {{ job_info.completed_count }}/{{ job_info.total }} complétées, {{ job_info.error_count }} erreurs)
 
 
61
  </li>
62
  {% else %}
63
  <li>Aucune tâche récente.</li>
@@ -76,27 +96,31 @@ HTML_STATUS = """
76
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
77
  <title>Statut Tâche {{ job_id }}</title>
78
  <style>
79
- body { font-family: sans-serif; margin: 20px; }
80
- #progress-bar-container { width: 100%; background-color: #f0f0f0; border-radius: 5px; margin-bottom: 10px; }
81
- #progress-bar { width: 0%; height: 30px; background-color: #4CAF50; text-align: center; line-height: 30px; color: white; border-radius: 5px; transition: width 0.5s ease-in-out; }
82
- .error-log { margin-top: 15px; max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; background-color: #f9f9f9;}
83
- .error-log p { margin: 5px 0; font-size: 0.9em; }
84
- .status-message { font-weight: bold; margin-bottom: 15px; }
 
 
 
 
85
  </style>
86
  </head>
87
  <body>
88
  <h1>Statut de la Tâche : {{ job_id }}</h1>
89
- <div id="status-message" class="status-message">Chargement...</div>
90
  <div id="progress-bar-container">
91
  <div id="progress-bar">0%</div>
92
  </div>
93
- <p>Requêtes complétées : <span id="completed">0</span> / <span id="total">?</span></p>
94
- <p>Erreurs : <span id="errors">0</span></p>
95
  <div id="error-details" class="error-log" style="display: none;">
96
  <h3>Détails des erreurs :</h3>
97
  <div id="error-list"></div>
98
  </div>
99
- <p><a href="/">Retour à l'accueil</a></p>
100
 
101
  <script>
102
  const jobId = "{{ job_id }}";
@@ -109,38 +133,55 @@ HTML_STATUS = """
109
  const errorListEl = document.getElementById('error-list');
110
 
111
  let intervalId = null;
 
112
 
113
  function updateStatus() {
114
  fetch(`/api/status/${jobId}`)
115
  .then(response => {
116
  if (!response.ok) {
 
 
 
 
 
117
  throw new Error(`Erreur HTTP: ${response.status}`);
118
  }
119
  return response.json();
120
  })
121
  .then(data => {
122
- if (!data) { // Gère le cas la tâche n'est pas encore prête
123
- statusMessageEl.textContent = "En attente de démarrage...";
124
  return;
125
  }
126
 
 
 
 
 
 
 
 
 
 
127
  completedEl.textContent = data.completed_count;
128
  totalEl.textContent = data.total;
129
  errorsEl.textContent = data.error_count;
130
- statusMessageEl.textContent = `Statut : ${data.status}`;
131
 
132
  let percentage = 0;
133
  if (data.total > 0) {
 
134
  percentage = Math.round((data.completed_count / data.total) * 100);
135
  }
136
  progressBarEl.style.width = percentage + '%';
137
  progressBarEl.textContent = percentage + '%';
138
 
139
- // Afficher les erreurs
140
  if (data.error_count > 0 && data.errors && data.errors.length > 0) {
141
- errorListEl.innerHTML = ''; // Clear previous errors
142
  data.errors.forEach(err => {
143
  const p = document.createElement('p');
 
144
  p.textContent = `Req ${err.index}: ${err.error}`;
145
  errorListEl.appendChild(p);
146
  });
@@ -149,35 +190,47 @@ HTML_STATUS = """
149
  errorDetailsEl.style.display = 'none';
150
  }
151
 
152
-
153
- if (data.status === 'completed' || data.status === 'failed') {
154
  if (intervalId) {
155
  clearInterval(intervalId);
156
- intervalId = null; // Arrête les mises à jour
157
- console.log("Mises à jour arrêtées car la tâche est terminée.");
158
- if (data.status === 'completed' && data.error_count == 0) {
159
- progressBarEl.style.backgroundColor = '#4CAF50'; // Vert
160
- } else if (data.status === 'completed' && data.error_count > 0) {
161
- progressBarEl.style.backgroundColor = '#ff9800'; // Orange
162
- } else { // failed
163
- progressBarEl.style.backgroundColor = '#f44336'; // Rouge
164
- }
165
  }
 
 
 
 
 
 
 
 
 
 
 
166
  }
167
  })
168
  .catch(error => {
169
  console.error("Erreur lors de la récupération du statut:", error);
170
- statusMessageEl.textContent = "Erreur lors de la récupération du statut.";
 
 
 
171
  if (intervalId) {
172
- clearInterval(intervalId); // Arrête en cas d'erreur persistante
173
  intervalId = null;
 
174
  }
 
 
175
  });
176
  }
177
 
178
- // Mettre à jour immédiatement puis toutes les 2 secondes
179
  updateStatus();
180
- intervalId = setInterval(updateStatus, 2000);
 
 
181
  </script>
182
  </body>
183
  </html>
@@ -185,132 +238,227 @@ HTML_STATUS = """
185
 
186
  # --- Fonctions Logiques ---
187
 
188
- def send_single_request(target_url, payload, job_id, request_index):
189
- """Fonction pour envoyer UNE requête POST."""
190
- # Crée une copie pour éviter de modifier l'original et pour ajouter un identifiant
 
 
 
 
 
 
 
 
 
 
 
 
191
  current_payload = copy.deepcopy(payload)
192
- current_payload['message'] += f" (Requête {request_index + 1})" # Ajoute un numéro à chaque message
193
- current_payload['request_uuid'] = str(uuid.uuid4()) # Ajoute un id unique par requête
 
 
 
 
 
194
 
195
  try:
196
- response = requests.post(target_url, json=current_payload, timeout=30) # Timeout de 30s
197
- response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP (4xx, 5xx)
198
- return True, None # Succès
 
 
 
 
 
 
 
 
 
199
  except requests.exceptions.RequestException as e:
200
- print(f"Erreur requête {request_index + 1} pour job {job_id}: {e}")
201
- return False, f"Req {request_index + 1}: {str(e)}" # Échec avec message d'erreur
202
-
203
- def background_task(job_id, num_requests, target_url, base_payload):
204
- """Fonction exécutée dans un thread séparé pour envoyer les requêtes."""
205
- print(f"Tâche {job_id}: Démarrage de {num_requests} requêtes vers {target_url}")
206
- completed_count = 0
207
- error_count = 0
208
- error_messages = []
209
-
210
- # Initialiser le statut (on le fait déjà dans /start mais re-vérifier est ok)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  with jobs_lock:
212
- if job_id not in jobs:
213
- jobs[job_id] = {
214
- 'status': 'running',
215
- 'total': num_requests,
216
- 'completed_count': 0,
217
- 'error_count': 0,
218
- 'errors': []
219
- }
220
 
221
  for i in range(num_requests):
222
  success, error_msg = send_single_request(target_url, base_payload, job_id, i)
223
- completed_count += 1
224
- if not success:
225
- error_count += 1
226
- error_messages.append({'index': i + 1, 'error': error_msg})
227
 
228
- # Mettre à jour la progression dans le dictionnaire partagé (avec verrou)
229
  with jobs_lock:
230
- jobs[job_id]['completed_count'] = completed_count
231
- jobs[job_id]['error_count'] = error_count
232
- # Gardons seulement les X dernières erreurs pour éviter de saturer la mémoire
233
- jobs[job_id]['errors'] = error_messages[-50:] # Garde les 50 dernières erreurs
234
-
235
- # Petite pause optionnelle pour ne pas submerger la cible (ex: 0.1 seconde)
236
- # time.sleep(0.1)
237
-
238
- # Marquer la tâche comme terminée
239
- final_status = 'failed' if error_count == num_requests else ('completed' if error_count == 0 else 'completed_with_errors')
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  with jobs_lock:
241
- jobs[job_id]['status'] = final_status
 
 
 
 
 
 
 
 
242
 
243
- print(f"Tâche {job_id}: Terminé. {completed_count - error_count} succès, {error_count} erreurs.")
244
 
245
  # --- Routes Flask ---
246
 
247
  @app.route('/', methods=['GET'])
248
  def index():
249
- """Affiche la page d'accueil avec le formulaire."""
250
- # On passe une copie triée des jobs récents au template
251
  with jobs_lock:
252
- # Trie par exemple par clé (qui approxime l'ordre de création ici)
253
- # ou ajoute un timestamp à la création du job pour trier par date.
254
  sorted_jobs = dict(sorted(jobs.items(), reverse=True))
255
  return render_template_string(HTML_INDEX, jobs_list=sorted_jobs)
256
 
257
  @app.route('/start', methods=['POST'])
258
  def start_requests():
259
- """Reçoit le nombre de requêtes et lance la tâche en arrière-plan."""
 
 
 
 
 
 
 
260
  try:
261
- num_requests = int(request.form.get('num_requests'))
262
  if num_requests <= 0:
263
- raise ValueError("Le nombre de requêtes doit être positif.")
264
- except (TypeError, ValueError) as e:
265
- with jobs_lock:
 
 
 
 
266
  sorted_jobs = dict(sorted(jobs.items(), reverse=True))
267
- return render_template_string(HTML_INDEX, error=f"Nombre invalide: {e}", jobs_list=sorted_jobs), 400
268
 
269
- job_id = str(uuid.uuid4())[:8] # ID de tâche court et unique
 
270
 
271
- # Initialiser l'état de la tâche avant de démarrer le thread
272
  with jobs_lock:
273
  jobs[job_id] = {
274
- 'status': 'starting', # ou 'queued'
275
  'total': num_requests,
276
  'completed_count': 0,
277
  'error_count': 0,
278
  'errors': []
 
 
279
  }
280
 
281
- # Créer et démarrer le thread
282
- thread = threading.Thread(target=background_task, args=(job_id, num_requests, TARGET_URL, BASE_PAYLOAD))
283
- thread.daemon = True # Permet au programme principal de quitter même si des threads tournent
 
 
 
284
  thread.start()
285
 
286
- print(f"Nouvelle tâche démarrée avec ID: {job_id}")
287
- # Rediriger vers la page de statut de cette tâche
288
  return redirect(url_for('job_status', job_id=job_id))
289
 
290
  @app.route('/status/<job_id>', methods=['GET'])
291
- def job_status(job_id):
292
- """Affiche la page HTML de suivi pour une tâche spécifique."""
293
  with jobs_lock:
 
294
  if job_id not in jobs:
 
295
  return "Tâche non trouvée", 404
296
- # La page HTML utilisera l'API /api/status pour les mises à jour dynamiques
297
- return render_template_string(HTML_STATUS, job_id=job_id)
 
 
 
 
 
298
 
299
  @app.route('/api/status/<job_id>', methods=['GET'])
300
- def api_job_status(job_id):
301
- """Fournit l'état actuel d'une tâche au format JSON (pour le JavaScript)."""
 
 
 
302
  with jobs_lock:
 
303
  job_info = jobs.get(job_id)
304
 
305
  if job_info:
306
- # Renvoyer une copie pour éviter les modifs concurrentes pendant la sérialisation JSON
 
307
  return jsonify(copy.deepcopy(job_info))
308
  else:
309
- # Renvoyer une réponse JSON même pour une erreur 404
310
- return jsonify({"error": "Tâche non trouvée"}), 404
 
311
 
312
  # --- Démarrage de l'application ---
313
  if __name__ == '__main__':
314
- # Utilise host='0.0.0.0' pour rendre accessible depuis d'autres machines sur le réseau
315
- # Attention: debug=True ne doit PAS être utilisé en production
316
- app.run(host='0.0.0.0', port=5000, debug=True)
 
 
 
 
 
1
+ # -*- coding: utf-8 -*- # Bonne pratique d'ajouter l'encodage
2
+
3
+ # Importations nécessaires
4
+ from flask import Flask, request, render_template_string, jsonify, redirect, url_for
5
  import requests
6
  import threading
7
+ import uuid # Pour générer des identifiants uniques pour chaque tâche
8
  import time
9
+ import copy # Pour copier le payload pour chaque requête
10
+ from typing import Dict, Any, List, Tuple, Optional # Pour les annotations de type
 
11
 
12
  # --- Configuration ---
13
+ TARGET_URL: str = "https://hook.us1.make.com/zal5qn0ggbewmvtsbo2uenfno8tz3n56"
14
+ BASE_PAYLOAD: Dict[str, Any] = {
15
  "name": "Testeur Auto ",
16
+ "email": "[email protected]", # Ajout de '+auto' pour distinguer
17
  "company": "aragon Inc.",
18
  "message": "Ceci est un test automatisé via Flask.",
19
+ "date": "2023-10-27T10:30:00Z", # Tu pourrais rendre cette date dynamique si besoin
20
  "source": "http://simulateur-bsbs-flask.com"
21
  }
22
+
23
+ # Constantes pour les statuts (évite les fautes de frappe)
24
+ STATUS_STARTING: str = 'starting'
25
+ STATUS_RUNNING: str = 'running'
26
+ STATUS_COMPLETED: str = 'completed'
27
+ STATUS_FAILED: str = 'failed'
28
+ # Optional: STATUS_COMPLETED_WITH_ERRORS: str = 'completed_with_errors'
29
+
30
  # Structure pour stocker l'état des tâches (jobs) en mémoire
31
+ # Format: { 'job_id': {'status': str, 'total': int, 'completed_count': int, 'error_count': int, 'errors': List[Dict[str, Any]] } }
32
+ jobs: Dict[str, Dict[str, Any]] = {}
33
+ jobs_lock = threading.Lock() # Pour éviter les problèmes d'accès concurrents au dict jobs
34
+
35
+ app = Flask(__name__) # Correction de l'initialisation de Flask
36
 
37
+ # --- Templates HTML (inchangés, car ils semblent corrects) ---
38
 
39
  # Page d'accueil pour démarrer les requêtes
40
  HTML_INDEX = """
 
45
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
46
  <title>Lanceur de Requêtes</title>
47
  <style>
48
+ body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
49
+ h1, h2 { color: #555; }
50
+ label { display: block; margin-bottom: 5px; font-weight: bold; }
51
+ input[type=number] { width: 100px; padding: 8px; margin-bottom: 15px; border: 1px solid #ccc; border-radius: 4px; }
52
+ button { padding: 10px 15px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 4px; }
53
+ button:hover { background-color: #0056b3; }
54
+ .error { color: red; margin-top: 10px; font-weight: bold; }
55
+ ul { list-style: none; padding: 0; }
56
+ li { background-color: #fff; margin-bottom: 10px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
57
+ li a { text-decoration: none; color: #007bff; }
58
+ li a:hover { text-decoration: underline; }
59
+ .job-info { font-size: 0.9em; color: #666; }
60
  </style>
61
  </head>
62
  <body>
63
  <h1>Envoyer des Requêtes POST en Masse</h1>
64
+ <form method="POST" action="{{ url_for('start_requests') }}">
65
  <label for="num_requests">Nombre de requêtes à envoyer :</label>
66
+ <input type="number" id="num_requests" name="num_requests" min="1" required value="10">
67
  <button type="submit">Lancer les requêtes</button>
68
  </form>
69
  {% if error %}
70
  <p class="error">{{ error }}</p>
71
  {% endif %}
72
 
73
+ <h2>Tâches récentes :</h2>
74
  <ul>
75
  {% for job_id, job_info in jobs_list.items() %}
76
  <li>
77
  <a href="{{ url_for('job_status', job_id=job_id) }}">Tâche {{ job_id }}</a>
78
+ <span class="job-info">
79
+ (Statut : {{ job_info.status }}, {{ job_info.completed_count }}/{{ job_info.total }} requêtes traitées, {{ job_info.error_count }} erreurs)
80
+ </span>
81
  </li>
82
  {% else %}
83
  <li>Aucune tâche récente.</li>
 
96
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
97
  <title>Statut Tâche {{ job_id }}</title>
98
  <style>
99
+ body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
100
+ h1 { color: #555; }
101
+ #progress-bar-container { width: 100%; background-color: #e0e0e0; border-radius: 5px; margin-bottom: 10px; overflow: hidden; } /* Overflow hidden for border radius */
102
+ #progress-bar { width: 0%; height: 30px; background-color: #28a745; /* Green default */ text-align: center; line-height: 30px; color: white; border-radius: 0; /* Radius handled by container */ transition: width 0.5s ease-in-out, background-color 0.5s ease-in-out; }
103
+ .error-log { margin-top: 15px; max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; background-color: #f9f9f9; border-radius: 4px;}
104
+ .error-log h3 { margin-top: 0; }
105
+ .error-log p { margin: 5px 0; font-size: 0.9em; color: #dc3545; } /* Red error text */
106
+ .status-message { font-weight: bold; margin-bottom: 15px; padding: 10px; background-color: #e9ecef; border-radius: 4px; }
107
+ a { color: #007bff; text-decoration: none; }
108
+ a:hover { text-decoration: underline; }
109
  </style>
110
  </head>
111
  <body>
112
  <h1>Statut de la Tâche : {{ job_id }}</h1>
113
+ <div id="status-message" class="status-message">Chargement des informations...</div>
114
  <div id="progress-bar-container">
115
  <div id="progress-bar">0%</div>
116
  </div>
117
+ <p>Requêtes traitées : <span id="completed">0</span> / <span id="total">?</span></p>
118
+ <p>Erreurs rencontrées : <span id="errors">0</span></p>
119
  <div id="error-details" class="error-log" style="display: none;">
120
  <h3>Détails des erreurs :</h3>
121
  <div id="error-list"></div>
122
  </div>
123
+ <p><a href="{{ url_for('index') }}">Retour à l'accueil</a></p>
124
 
125
  <script>
126
  const jobId = "{{ job_id }}";
 
133
  const errorListEl = document.getElementById('error-list');
134
 
135
  let intervalId = null;
136
+ let lastStatus = ""; // Pour éviter des mises à jour inutiles si le statut n'a pas changé
137
 
138
  function updateStatus() {
139
  fetch(`/api/status/${jobId}`)
140
  .then(response => {
141
  if (!response.ok) {
142
+ // Si 404, la tâche n'existe pas (ou plus), arrêter les requêtes
143
+ if (response.status === 404) {
144
+ statusMessageEl.textContent = "Erreur: Tâche non trouvée.";
145
+ throw new Error('Job Not Found'); // Pour arrêter l'intervalle dans le catch
146
+ }
147
  throw new Error(`Erreur HTTP: ${response.status}`);
148
  }
149
  return response.json();
150
  })
151
  .then(data => {
152
+ if (!data) { // Devrait être géré par le 404 mais double sécurité
153
+ statusMessageEl.textContent = "En attente des données...";
154
  return;
155
  }
156
 
157
+ // Comparer avec le statut précédent pour éviter de redessiner si rien n'a changé
158
+ const currentDataSignature = `${data.status}-${data.completed_count}-${data.error_count}`;
159
+ if (currentDataSignature === lastStatus) {
160
+ // console.log("Pas de changement de statut détecté.");
161
+ return; // Rien à faire
162
+ }
163
+ lastStatus = currentDataSignature;
164
+
165
+ // Mise à jour des éléments DOM
166
  completedEl.textContent = data.completed_count;
167
  totalEl.textContent = data.total;
168
  errorsEl.textContent = data.error_count;
169
+ statusMessageEl.textContent = `Statut : ${data.status}`; // Utilise les constantes définies côté serveur
170
 
171
  let percentage = 0;
172
  if (data.total > 0) {
173
+ // On calcule le pourcentage sur le nombre total de requêtes à faire
174
  percentage = Math.round((data.completed_count / data.total) * 100);
175
  }
176
  progressBarEl.style.width = percentage + '%';
177
  progressBarEl.textContent = percentage + '%';
178
 
179
+ // Afficher les erreurs s'il y en a
180
  if (data.error_count > 0 && data.errors && data.errors.length > 0) {
181
+ errorListEl.innerHTML = ''; // Vider les erreurs précédentes
182
  data.errors.forEach(err => {
183
  const p = document.createElement('p');
184
+ // Utilisation de textContent pour éviter les injections XSS potentielles si les messages d'erreur contenaient du HTML
185
  p.textContent = `Req ${err.index}: ${err.error}`;
186
  errorListEl.appendChild(p);
187
  });
 
190
  errorDetailsEl.style.display = 'none';
191
  }
192
 
193
+ // Gérer la fin de la tâche
194
+ if (data.status === '{{ STATUS_COMPLETED }}' || data.status === '{{ STATUS_FAILED }}') {
195
  if (intervalId) {
196
  clearInterval(intervalId);
197
+ intervalId = null; // Arrête les mises à jour futures
198
+ app.logger.info(f"Mises à jour automatiques arrêtées pour la tâche {jobId} (statut final: {data.status}).");
 
 
 
 
 
 
 
199
  }
200
+ // Mettre à jour la couleur de la barre de progression en fonction du résultat final
201
+ if (data.status === '{{ STATUS_COMPLETED }}' && data.error_count == 0) {
202
+ progressBarEl.style.backgroundColor = '#28a745'; // Vert succès
203
+ } else if (data.status === '{{ STATUS_COMPLETED }}' && data.error_count > 0) {
204
+ progressBarEl.style.backgroundColor = '#ffc107'; // Jaune/Orange pour succès avec erreurs
205
+ } else { // STATUS_FAILED
206
+ progressBarEl.style.backgroundColor = '#dc3545'; // Rouge échec
207
+ }
208
+ } else {
209
+ // Si la tâche est en cours, s'assurer que la couleur est celle par défaut (ou une couleur "en cours")
210
+ progressBarEl.style.backgroundColor = '#007bff'; // Bleu pour en cours
211
  }
212
  })
213
  .catch(error => {
214
  console.error("Erreur lors de la récupération du statut:", error);
215
+ if (error.message !== 'Job Not Found') { // Ne pas écraser le message "Tâche non trouvée"
216
+ statusMessageEl.textContent = "Erreur lors de la récupération du statut.";
217
+ }
218
+ // Arrêter les mises à jour en cas d'erreur persistante ou de tâche non trouvée
219
  if (intervalId) {
220
+ clearInterval(intervalId);
221
  intervalId = null;
222
+ app.logger.error(f"Arrêt des mises à jour pour la tâche {jobId} suite à une erreur: {error}");
223
  }
224
+ // Optionnel: Changer la couleur de la barre en cas d'erreur de récupération
225
+ progressBarEl.style.backgroundColor = '#6c757d'; // Gris pour état indéterminé/erreur
226
  });
227
  }
228
 
229
+ // Démarrer la mise à jour : première mise à jour immédiate, puis toutes les 2 secondes
230
  updateStatus();
231
+ if (!intervalId && statusMessageEl.textContent !== "Erreur: Tâche non trouvée.") { // Ne pas démarrer l'intervalle si la tâche n'est déjà pas trouvée
232
+ intervalId = setInterval(updateStatus, 2000);
233
+ }
234
  </script>
235
  </body>
236
  </html>
 
238
 
239
  # --- Fonctions Logiques ---
240
 
241
+ def send_single_request(target_url: str, payload: Dict[str, Any], job_id: str, request_index: int) -> Tuple[bool, Optional[str]]:
242
+ """
243
+ Envoie UNE requête POST unique à la cible.
244
+
245
+ Args:
246
+ target_url: L'URL à laquelle envoyer la requête.
247
+ payload: Le dictionnaire de base du payload.
248
+ job_id: L'ID de la tâche parente (pour logs/debug).
249
+ request_index: L'index de cette requête dans la tâche (0-based).
250
+
251
+ Returns:
252
+ Un tuple (succès: bool, message_erreur: Optional[str]).
253
+ """
254
+ # Crée une copie profonde pour éviter de modifier l'original entre les threads
255
+ # et pour ajouter des informations spécifiques à cette requête.
256
  current_payload = copy.deepcopy(payload)
257
+ # Personnalisation du message pour identifier la requête spécifique
258
+ current_payload['message'] += f" (Requête {request_index + 1} / Job {job_id})"
259
+ # Ajout d'un UUID unique à chaque requête pour un suivi fin si nécessaire côté serveur cible
260
+ current_payload['request_uuid'] = str(uuid.uuid4())
261
+ # Optionnel: Mettre à jour la date/heure au moment de l'envoi
262
+ # from datetime import datetime, timezone
263
+ # current_payload['date'] = datetime.now(timezone.utc).isoformat()
264
 
265
  try:
266
+ response = requests.post(target_url, json=current_payload, timeout=30) # Timeout de 30 secondes
267
+ response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP (4xx, 5xx)
268
+ # app.logger.debug(f"Job {job_id} - Requête {request_index + 1}: Succès (Status {response.status_code})")
269
+ return True, None # Succès
270
+ except requests.exceptions.Timeout:
271
+ error_msg = f"Timeout après 30s pour Req {request_index + 1}"
272
+ app.logger.warning(f"Job {job_id}: {error_msg}")
273
+ return False, error_msg
274
+ except requests.exceptions.HTTPError as e:
275
+ error_msg = f"Erreur HTTP {e.response.status_code} pour Req {request_index + 1}: {e.response.reason}"
276
+ app.logger.warning(f"Job {job_id}: {error_msg} - Réponse: {e.response.text[:200]}") # Log début de réponse
277
+ return False, error_msg
278
  except requests.exceptions.RequestException as e:
279
+ # Capture les autres erreurs (DNS, connexion refusée, etc.)
280
+ error_msg = f"Erreur Réseau/Requête pour Req {request_index + 1}: {str(e)}"
281
+ app.logger.error(f"Job {job_id}: {error_msg}")
282
+ return False, error_msg # Échec avec message d'erreur
283
+
284
+ def background_task(job_id: str, num_requests: int, target_url: str, base_payload: Dict[str, Any]):
285
+ """
286
+ Fonction exécutée dans un thread séparé pour envoyer toutes les requêtes d'une tâche.
287
+ Met à jour l'état de la tâche dans le dictionnaire partagé `jobs`.
288
+ """
289
+ app.logger.info(f"Tâche {job_id}: Démarrage de {num_requests} requêtes vers {target_url}")
290
+ completed_count: int = 0
291
+ error_count: int = 0
292
+ error_messages: List[Dict[str, Any]] = []
293
+
294
+ # Note: L'initialisation de la tâche est maintenant faite dans /start avant le lancement du thread.
295
+ # La partie ci-dessous est redondante mais laissée commentée pour info.
296
+ # # Vérifier/Initialiser le statut (au cas où, bien que fait dans /start)
297
+ # with jobs_lock:
298
+ # if job_id not in jobs:
299
+ # app.logger.warning(f"Tâche {job_id} non trouvée dans jobs au démarrage du thread. Initialisation.")
300
+ # jobs[job_id] = {
301
+ # 'status': STATUS_RUNNING, # On la met directement en running
302
+ # 'total': num_requests,
303
+ # 'completed_count': 0,
304
+ # 'error_count': 0,
305
+ # 'errors': []
306
+ # }
307
+ # else:
308
+ # # Si elle existe déjà (normal), s'assurer qu'elle est en running
309
+ # jobs[job_id]['status'] = STATUS_RUNNING
310
+
311
+ # Mettre à jour le statut en 'running' une fois démarré
312
  with jobs_lock:
313
+ if job_id in jobs:
314
+ jobs[job_id]['status'] = STATUS_RUNNING
315
+ else:
316
+ # Cas très improbable si /start n'a pas fini avant que le thread ne check
317
+ app.logger.error(f"Tâche {job_id} non trouvée au moment de passer en status RUNNING.")
318
+ return # Arrêter le thread si la tâche n'existe pas
 
 
319
 
320
  for i in range(num_requests):
321
  success, error_msg = send_single_request(target_url, base_payload, job_id, i)
 
 
 
 
322
 
323
+ # Mettre à jour la progression dans le dictionnaire partagé (section critique)
324
  with jobs_lock:
325
+ # Vérifier si la tâche existe toujours (elle pourrait être supprimée?)
326
+ if job_id not in jobs:
327
+ app.logger.warning(f"Tâche {job_id} disparue pendant l'exécution. Arrêt.")
328
+ break # Sortir de la boucle si la tâche n'est plus suivie
329
+
330
+ # Incrémenter le compteur des requêtes traitées
331
+ jobs[job_id]['completed_count'] += 1
332
+ completed_count = jobs[job_id]['completed_count'] # Mettre à jour la variable locale aussi
333
+
334
+ if not success:
335
+ jobs[job_id]['error_count'] += 1
336
+ error_count = jobs[job_id]['error_count'] # Mettre à jour la variable locale
337
+ # Ajouter les détails de l'erreur (index basé sur 1 pour l'affichage)
338
+ error_detail = {'index': i + 1, 'error': error_msg or "Erreur inconnue"}
339
+ jobs[job_id]['errors'].append(error_detail)
340
+ # Gardons seulement les X dernières erreurs pour éviter de saturer la mémoire
341
+ max_errors_to_keep = 100
342
+ jobs[job_id]['errors'] = jobs[job_id]['errors'][-max_errors_to_keep:]
343
+
344
+ # Petite pause optionnelle pour ne pas surcharger la cible ou le réseau local
345
+ # time.sleep(0.05) # 50ms pause
346
+
347
+ # Marquer la tâche comme terminée une fois la boucle finie
348
  with jobs_lock:
349
+ if job_id in jobs:
350
+ # Déterminer le statut final basé sur les erreurs
351
+ # Si toutes les requêtes ont échoué -> FAILED
352
+ # Sinon -> COMPLETED (le nombre d'erreurs indique si c'était parfait ou non)
353
+ final_status = STATUS_FAILED if error_count == num_requests and num_requests > 0 else STATUS_COMPLETED
354
+ jobs[job_id]['status'] = final_status
355
+ app.logger.info(f"Tâche {job_id}: Terminé. Statut final: {final_status}. {completed_count - error_count}/{num_requests} succès, {error_count} erreurs.")
356
+ else:
357
+ app.logger.warning(f"Tâche {job_id} non trouvée à la fin de l'exécution pour marquer comme terminée.")
358
 
 
359
 
360
  # --- Routes Flask ---
361
 
362
  @app.route('/', methods=['GET'])
363
  def index():
364
+ """Affiche la page d'accueil avec le formulaire et la liste des tâches."""
 
365
  with jobs_lock:
366
+ # Trie les tâches par clé (ID), en ordre inverse (plus récent d'abord si UUID approxime le temps)
367
+ # Pour un tri chronologique fiable, il faudrait ajouter un timestamp lors de la création de la tâche.
368
  sorted_jobs = dict(sorted(jobs.items(), reverse=True))
369
  return render_template_string(HTML_INDEX, jobs_list=sorted_jobs)
370
 
371
  @app.route('/start', methods=['POST'])
372
  def start_requests():
373
+ """
374
+ Reçoit le nombre de requêtes depuis le formulaire,
375
+ valide l'entrée, crée une nouvelle tâche et lance le thread d'arrière-plan.
376
+ Redirige vers la page de statut de la nouvelle tâche.
377
+ """
378
+ num_requests_str = request.form.get('num_requests')
379
+ num_requests: int = 0
380
+
381
  try:
382
+ num_requests = int(num_requests_str)
383
  if num_requests <= 0:
384
+ raise ValueError("Le nombre de requêtes doit être un entier positif.")
385
+ if num_requests > 10000: # Limite de sécurité (optionnelle)
386
+ raise ValueError("Le nombre de requêtes est limité à 10000.")
387
+ except (TypeError, ValueError, AttributeError) as e:
388
+ app.logger.warning(f"Tentative de démarrage échouée - nombre invalide: '{num_requests_str}' - Erreur: {e}")
389
+ # Correction: Il faut re-passer la liste des jobs au template d'index en cas d'erreur
390
+ with jobs_lock:
391
  sorted_jobs = dict(sorted(jobs.items(), reverse=True))
392
+ return render_template_string(HTML_INDEX, error=f"Nombre de requêtes invalide : {e}", jobs_list=sorted_jobs), 400
393
 
394
+ # Générer un ID de tâche unique (partie courte d'un UUID v4)
395
+ job_id: str = str(uuid.uuid4())[:8]
396
 
397
+ # Initialiser l'état de la tâche dans le dictionnaire partagé (section critique)
398
  with jobs_lock:
399
  jobs[job_id] = {
400
+ 'status': STATUS_STARTING, # Statut initial avant que le thread ne démarre vraiment
401
  'total': num_requests,
402
  'completed_count': 0,
403
  'error_count': 0,
404
  'errors': []
405
+ # Optionnel: Ajouter un timestamp de création
406
+ # 'created_at': datetime.now(timezone.utc).isoformat()
407
  }
408
 
409
+ # Créer et démarrer le thread pour exécuter la tâche en arrière-plan
410
+ thread = threading.Thread(
411
+ target=background_task,
412
+ args=(job_id, num_requests, TARGET_URL, BASE_PAYLOAD),
413
+ daemon=True # Permet au programme principal de quitter même si des threads sont encore en cours d'exécution
414
+ )
415
  thread.start()
416
 
417
+ app.logger.info(f"Nouvelle tâche {job_id} démarrée pour {num_requests} requêtes.")
418
+ # Rediriger l'utilisateur vers la page de statut de cette nouvelle tâche
419
  return redirect(url_for('job_status', job_id=job_id))
420
 
421
  @app.route('/status/<job_id>', methods=['GET'])
422
+ def job_status(job_id: str):
423
+ """Affiche la page HTML de suivi pour une tâche spécifique (identifiée par job_id)."""
424
  with jobs_lock:
425
+ # Vérifier si la tâche existe juste pour éviter d'afficher une page pour un ID invalide
426
  if job_id not in jobs:
427
+ app.logger.warning(f"Tentative d'accès à la page de statut pour une tâche inexistante: {job_id}")
428
  return "Tâche non trouvée", 404
429
+
430
+ # La page HTML contient le JavaScript qui appellera l'API '/api/status/<job_id>'
431
+ # pour obtenir les données de progression dynamiquement.
432
+ # On passe les constantes de statut au template pour que le JS puisse les utiliser
433
+ return render_template_string(HTML_STATUS, job_id=job_id,
434
+ STATUS_COMPLETED=STATUS_COMPLETED,
435
+ STATUS_FAILED=STATUS_FAILED)
436
 
437
  @app.route('/api/status/<job_id>', methods=['GET'])
438
+ def api_job_status(job_id: str):
439
+ """
440
+ Fournit l'état actuel d'une tâche spécifique au format JSON.
441
+ Utilisé par le JavaScript de la page de statut pour les mises à jour.
442
+ """
443
  with jobs_lock:
444
+ # Obtenir les informations de la tâche. Utiliser .get() pour gérer le cas où l'ID n'existe pas.
445
  job_info = jobs.get(job_id)
446
 
447
  if job_info:
448
+ # Renvoyer une copie profonde pour éviter toute modification concurrente pendant la sérialisation JSON
449
+ # Bien que le lock aide, c'est une sécurité supplémentaire, surtout si les structures de données deviennent complexes.
450
  return jsonify(copy.deepcopy(job_info))
451
  else:
452
+ # Si la tâche n'est pas trouvée, renvoyer une réponse JSON avec une erreur 404.
453
+ app.logger.warning(f"API: Statut demandé pour tâche inexistante: {job_id}")
454
+ return jsonify({"error": "Tâche non trouvée", "job_id": job_id}), 404
455
 
456
  # --- Démarrage de l'application ---
457
  if __name__ == '__main__':
458
+ # Utiliser host='0.0.0.0' pour rendre l'application accessible
459
+ # depuis d'autres machines sur le même réseau local.
460
+ # ATTENTION : debug=True ne doit JAMAIS être utilisé dans un environnement de production.
461
+ # Il expose des vulnérabilités de sécurité et affecte les performances.
462
+ print("Démarrage du serveur Flask...")
463
+ print(f"Accéder à l'application via http://localhost:5000 ou http://<votre_ip_locale>:5000")
464
+ app.run(host='0.0.0.0', port=5000, debug=True) # Mettre debug=False pour la production