Harshit Ghosh Copilot commited on
Commit
46811bc
Β·
1 Parent(s): 5105d0e

progress and cancel

Browse files

Co-authored-by: Copilot <copilot@github.com>

app_new.py CHANGED
@@ -1056,7 +1056,7 @@ def analyze():
1056
  user_id=current_user.id,
1057
  details=f"batch_id={batch_id}, files={len(dcm_paths)}",
1058
  )
1059
- return redirect(url_for("batch_progress", batch_id=batch_id))
1060
  except Exception:
1061
  logger.error("Celery unavailable; running synchronous fallback", exc_info=True)
1062
  flash("Celery worker unavailable. Running batch synchronously; this may take a while.", "warning")
@@ -1097,7 +1097,7 @@ def analyze_directory():
1097
  user_id=current_user.id,
1098
  details=f"batch_id={batch_id}, files={len(dcm_paths)}",
1099
  )
1100
- return redirect(url_for("batch_progress", batch_id=batch_id))
1101
  except Exception:
1102
  logger.error("Celery unavailable; running synchronous directory scan", exc_info=True)
1103
  flash("Celery worker unavailable. Running directory scan synchronously.", "warning")
@@ -1115,8 +1115,15 @@ def batch_progress(batch_id):
1115
  batch = _get_batch_from_celery(batch_id)
1116
  if not batch or batch.get("user_id") != current_user.id:
1117
  abort(404)
1118
-
1119
- return render_template("batch_progress.html", batch=batch, batch_id=batch_id)
 
 
 
 
 
 
 
1120
 
1121
  @app.route("/batch/<batch_id>/status")
1122
  @login_required
@@ -1127,6 +1134,26 @@ def batch_status(batch_id):
1127
  return jsonify({"error": "Not found"}), 404
1128
  return jsonify(batch)
1129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1130
  def _get_batch_from_celery(batch_id: str) -> dict[str, Any] | None:
1131
  """Retrieve batch status from Celery task result backend."""
1132
  # In a production system, we'd also validate user_id from the database
@@ -1155,6 +1182,22 @@ def _get_batch_from_celery(batch_id: str) -> dict[str, Any] | None:
1155
  "error": None,
1156
  "queue_size": queue_size,
1157
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1158
 
1159
  # Build response matching _BATCHES format for frontend compatibility
1160
  if result.state == "PROGRESS":
 
1056
  user_id=current_user.id,
1057
  details=f"batch_id={batch_id}, files={len(dcm_paths)}",
1058
  )
1059
+ return redirect(url_for("batch_progress", batch_id=batch_id, total=len(dcm_paths)))
1060
  except Exception:
1061
  logger.error("Celery unavailable; running synchronous fallback", exc_info=True)
1062
  flash("Celery worker unavailable. Running batch synchronously; this may take a while.", "warning")
 
1097
  user_id=current_user.id,
1098
  details=f"batch_id={batch_id}, files={len(dcm_paths)}",
1099
  )
1100
+ return redirect(url_for("batch_progress", batch_id=batch_id, total=len(dcm_paths)))
1101
  except Exception:
1102
  logger.error("Celery unavailable; running synchronous directory scan", exc_info=True)
1103
  flash("Celery worker unavailable. Running directory scan synchronously.", "warning")
 
1115
  batch = _get_batch_from_celery(batch_id)
1116
  if not batch or batch.get("user_id") != current_user.id:
1117
  abort(404)
1118
+ expected_total = request.args.get("total", type=int)
1119
+ if expected_total and (batch.get("total") or 0) == 0:
1120
+ batch["total"] = expected_total
1121
+ return render_template(
1122
+ "batch_progress.html",
1123
+ batch=batch,
1124
+ batch_id=batch_id,
1125
+ expected_total=expected_total or 0,
1126
+ )
1127
 
1128
  @app.route("/batch/<batch_id>/status")
1129
  @login_required
 
1134
  return jsonify({"error": "Not found"}), 404
1135
  return jsonify(batch)
1136
 
1137
+ @app.route("/batch/<batch_id>/cancel", methods=["POST"])
1138
+ @login_required
1139
+ def cancel_batch(batch_id):
1140
+ """Cancel a running batch task."""
1141
+ user_id = _extract_user_id_from_batch_id(batch_id)
1142
+ if user_id != current_user.id:
1143
+ abort(404)
1144
+ try:
1145
+ celery_app.control.revoke(batch_id, terminate=True, signal="SIGTERM")
1146
+ log_audit(
1147
+ "batch_canceled",
1148
+ user_id=current_user.id,
1149
+ details=f"batch_id={batch_id}",
1150
+ status="success",
1151
+ )
1152
+ return jsonify({"status": "canceled"})
1153
+ except Exception as exc:
1154
+ logger.error("Failed to cancel batch %s: %s", batch_id, exc, exc_info=True)
1155
+ return jsonify({"error": "Cancel failed"}), 500
1156
+
1157
  def _get_batch_from_celery(batch_id: str) -> dict[str, Any] | None:
1158
  """Retrieve batch status from Celery task result backend."""
1159
  # In a production system, we'd also validate user_id from the database
 
1182
  "error": None,
1183
  "queue_size": queue_size,
1184
  }
1185
+ elif result.state == "REVOKED":
1186
+ return {
1187
+ "batch_id": batch_id,
1188
+ "user_id": user_id,
1189
+ "status": "canceled",
1190
+ "total": 0,
1191
+ "processed": 0,
1192
+ "succeeded": 0,
1193
+ "failed_ids": [],
1194
+ "image_ids": [],
1195
+ "current_file": "",
1196
+ "started_at": None,
1197
+ "finished_at": None,
1198
+ "error": None,
1199
+ "queue_size": queue_size,
1200
+ }
1201
 
1202
  # Build response matching _BATCHES format for frontend compatibility
1203
  if result.state == "PROGRESS":
auth_routes.py CHANGED
@@ -92,6 +92,9 @@ def _validate_otp(submitted_code: str, expected_purpose: str) -> tuple[bool, str
92
  return False, "OTP purpose mismatch. Please request a new code.", None
93
 
94
  expires_raw = payload.get("expires_at")
 
 
 
95
  try:
96
  expires_at = datetime.fromisoformat(expires_raw)
97
  except Exception:
 
92
  return False, "OTP purpose mismatch. Please request a new code.", None
93
 
94
  expires_raw = payload.get("expires_at")
95
+ if not expires_raw:
96
+ _clear_otp()
97
+ return False, "OTP is invalid. Please request a new code.", None
98
  try:
99
  expires_at = datetime.fromisoformat(expires_raw)
100
  except Exception:
static/css/pages.css CHANGED
@@ -963,6 +963,11 @@
963
  .batch-header {
964
  margin-bottom: 20px;
965
  }
 
 
 
 
 
966
  .batch-header h1 {
967
  font-size: 1.6rem;
968
  font-weight: 800;
 
963
  .batch-header {
964
  margin-bottom: 20px;
965
  }
966
+ .batch-header-actions {
967
+ margin-top: 14px;
968
+ display: flex;
969
+ justify-content: flex-end;
970
+ }
971
  .batch-header h1 {
972
  font-size: 1.6rem;
973
  font-weight: 800;
static/js/batch.js CHANGED
@@ -7,6 +7,8 @@
7
 
8
  var statusUrl = page.dataset.statusUrl;
9
  var pollMs = 1000;
 
 
10
 
11
  var title = document.getElementById('batchTitle');
12
  var subtitle = document.getElementById('batchSubtitle');
@@ -24,21 +26,51 @@
24
  var doneSummary = document.getElementById('doneSummary');
25
  var failPanel = document.getElementById('failPanel');
26
  var failList = document.getElementById('failList');
 
27
  var prevIds = [];
 
28
 
29
  if (!statusUrl || !title || !subtitle || !fill || !pctLabel || !currentFile || !statTotal || !statProc || !statOK || !statFail || !feedPanel || !feedList || !donePanel || !doneSummary || !failPanel || !failList) {
30
  return;
31
  }
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  function poll() {
34
  fetch(statusUrl)
35
  .then(function (response) {
36
  return response.json();
37
  })
38
  .then(function (data) {
39
- var pct = data.total > 0 ? Math.round(data.processed / data.total * 100) : 0;
 
 
40
 
41
- statTotal.textContent = data.total;
 
 
 
42
  statProc.textContent = data.processed;
43
  statOK.textContent = data.succeeded;
44
  statFail.textContent = data.failed_ids ? data.failed_ids.length : 0;
@@ -53,6 +85,10 @@
53
  }
54
  }
55
 
 
 
 
 
56
  fill.style.width = pct + '%';
57
  pctLabel.textContent = pct + '%';
58
  currentFile.textContent = data.current_file ? 'Processing: ' + data.current_file : '';
@@ -75,12 +111,18 @@
75
  });
76
  }
77
 
 
 
 
 
 
 
78
  if (data.status === 'completed' || data.status === 'failed') {
79
  title.textContent = 'Batch Complete';
80
  subtitle.textContent = '';
81
  donePanel.style.display = 'block';
82
  var failCount = data.failed_ids ? data.failed_ids.length : 0;
83
- doneSummary.textContent = data.succeeded + ' of ' + data.total + ' files processed successfully' + (failCount > 0 ? ', ' + failCount + ' failed' : '') + '.';
84
 
85
  if (data.failed_ids && data.failed_ids.length) {
86
  failPanel.style.display = 'block';
 
7
 
8
  var statusUrl = page.dataset.statusUrl;
9
  var pollMs = 1000;
10
+ var expectedTotal = parseInt(page.dataset.expectedTotal || '0', 10) || 0;
11
+ var cancelUrl = page.dataset.cancelUrl;
12
 
13
  var title = document.getElementById('batchTitle');
14
  var subtitle = document.getElementById('batchSubtitle');
 
26
  var doneSummary = document.getElementById('doneSummary');
27
  var failPanel = document.getElementById('failPanel');
28
  var failList = document.getElementById('failList');
29
+ var cancelBtn = document.getElementById('cancelBatch');
30
  var prevIds = [];
31
+ var canceled = false;
32
 
33
  if (!statusUrl || !title || !subtitle || !fill || !pctLabel || !currentFile || !statTotal || !statProc || !statOK || !statFail || !feedPanel || !feedList || !donePanel || !doneSummary || !failPanel || !failList) {
34
  return;
35
  }
36
 
37
+ if (cancelBtn && cancelUrl) {
38
+ cancelBtn.addEventListener('click', function () {
39
+ if (!confirm('Cancel this batch? Any in-progress file may not complete.')) {
40
+ return;
41
+ }
42
+ cancelBtn.disabled = true;
43
+ fetch(cancelUrl, { method: 'POST' })
44
+ .then(function (response) { return response.json(); })
45
+ .then(function (data) {
46
+ canceled = true;
47
+ title.textContent = 'Batch Canceled';
48
+ subtitle.textContent = 'The batch was canceled. You can start a new upload anytime.';
49
+ currentFile.textContent = '';
50
+ if (queueStatus) {
51
+ queueStatus.textContent = '';
52
+ }
53
+ })
54
+ .catch(function () {
55
+ cancelBtn.disabled = false;
56
+ });
57
+ });
58
+ }
59
+
60
  function poll() {
61
  fetch(statusUrl)
62
  .then(function (response) {
63
  return response.json();
64
  })
65
  .then(function (data) {
66
+ if (canceled) {
67
+ return;
68
+ }
69
 
70
+ var total = data.total > 0 ? data.total : expectedTotal;
71
+ var pct = total > 0 ? Math.round(data.processed / total * 100) : 0;
72
+
73
+ statTotal.textContent = total;
74
  statProc.textContent = data.processed;
75
  statOK.textContent = data.succeeded;
76
  statFail.textContent = data.failed_ids ? data.failed_ids.length : 0;
 
85
  }
86
  }
87
 
88
+ if (data.status === 'pending' && expectedTotal > 0) {
89
+ subtitle.textContent = 'Queued - waiting for a worker. Total files: ' + expectedTotal + '.';
90
+ }
91
+
92
  fill.style.width = pct + '%';
93
  pctLabel.textContent = pct + '%';
94
  currentFile.textContent = data.current_file ? 'Processing: ' + data.current_file : '';
 
111
  });
112
  }
113
 
114
+ if (data.status === 'canceled') {
115
+ title.textContent = 'Batch Canceled';
116
+ subtitle.textContent = 'The batch was canceled.';
117
+ return;
118
+ }
119
+
120
  if (data.status === 'completed' || data.status === 'failed') {
121
  title.textContent = 'Batch Complete';
122
  subtitle.textContent = '';
123
  donePanel.style.display = 'block';
124
  var failCount = data.failed_ids ? data.failed_ids.length : 0;
125
+ doneSummary.textContent = data.succeeded + ' of ' + total + ' files processed successfully' + (failCount > 0 ? ', ' + failCount + ' failed' : '') + '.';
126
 
127
  if (data.failed_ids && data.failed_ids.length) {
128
  failPanel.style.display = 'block';
templates/batch_progress.html CHANGED
@@ -3,7 +3,13 @@
3
  {% block title %}Batch Processing β€” AI Medical Intelligence Pipeline{% endblock %}
4
 
5
  {% block content %}
6
- <div class="batch-page" data-batch-id="{{ batch_id }}" data-status-url="{{ url_for('batch_status', batch_id=batch_id) }}" data-reports-url="{{ url_for('reports') }}">
 
 
 
 
 
 
7
  <section class="breadcrumb">
8
  <a href="{{ url_for('home') }}">Home</a>
9
  <span class="sep">/</span>
@@ -15,13 +21,16 @@
15
  <section class="batch-header">
16
  <h1 id="batchTitle">Processing Batch&hellip;</h1>
17
  <p class="muted" id="batchSubtitle">
18
- Analyzing {{ batch.total }} DICOM file{{ 's' if batch.total != 1 }} β€” please keep this page open.
19
  </p>
20
  <p class="muted" id="queueStatus">
21
  {% if batch.queue_size is not none %}
22
  Queue size: {{ batch.queue_size }}
23
  {% endif %}
24
  </p>
 
 
 
25
  </section>
26
 
27
  <!-- ── Progress bar ────────────────────────────────────────────────── -->
@@ -29,7 +38,7 @@
29
  <div class="batch-stats-row">
30
  <div class="batch-stat">
31
  <span class="batch-stat-label">Total</span>
32
- <span class="batch-stat-value" id="statTotal">{{ batch.total }}</span>
33
  </div>
34
  <div class="batch-stat">
35
  <span class="batch-stat-label">Processed</span>
 
3
  {% block title %}Batch Processing β€” AI Medical Intelligence Pipeline{% endblock %}
4
 
5
  {% block content %}
6
+ {% set display_total = batch.total if batch.total else (expected_total or 0) %}
7
+ <div class="batch-page"
8
+ data-batch-id="{{ batch_id }}"
9
+ data-status-url="{{ url_for('batch_status', batch_id=batch_id) }}"
10
+ data-cancel-url="{{ url_for('cancel_batch', batch_id=batch_id) }}"
11
+ data-reports-url="{{ url_for('reports') }}"
12
+ data-expected-total="{{ display_total }}">
13
  <section class="breadcrumb">
14
  <a href="{{ url_for('home') }}">Home</a>
15
  <span class="sep">/</span>
 
21
  <section class="batch-header">
22
  <h1 id="batchTitle">Processing Batch&hellip;</h1>
23
  <p class="muted" id="batchSubtitle">
24
+ Analyzing {{ display_total }} DICOM file{{ 's' if display_total != 1 }} β€” please keep this page open.
25
  </p>
26
  <p class="muted" id="queueStatus">
27
  {% if batch.queue_size is not none %}
28
  Queue size: {{ batch.queue_size }}
29
  {% endif %}
30
  </p>
31
+ <div class="batch-header-actions">
32
+ <button type="button" class="btn btn-outline" id="cancelBatch">Cancel Batch</button>
33
+ </div>
34
  </section>
35
 
36
  <!-- ── Progress bar ────────────────────────────────────────────────── -->
 
38
  <div class="batch-stats-row">
39
  <div class="batch-stat">
40
  <span class="batch-stat-label">Total</span>
41
+ <span class="batch-stat-value" id="statTotal">{{ display_total }}</span>
42
  </div>
43
  <div class="batch-stat">
44
  <span class="batch-stat-label">Processed</span>