whassan commited on
Commit
23633b4
·
verified ·
1 Parent(s): 17fbf45

Deploy Sensorflow Studio (part 6)

Browse files
static/app.js CHANGED
@@ -2,6 +2,54 @@ document.addEventListener('DOMContentLoaded', () => {
2
  // API base URL
3
  const API_BASE = '';
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  // Stage Navigation
6
  const navButtons = document.querySelectorAll('.nav-btn');
7
  const panels = document.querySelectorAll('.stage-panel');
@@ -30,11 +78,27 @@ document.addEventListener('DOMContentLoaded', () => {
30
  let pipelineState = {};
31
  let currentSequenceId = 'seq_001';
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  async function refreshPipelineStatus() {
34
  try {
35
- const res = await fetch(`${API_BASE}/api/pipeline/status?sequence_id=${currentSequenceId}`);
 
36
  const data = await res.json();
37
  pipelineState = data;
 
38
  updateGateBadges();
39
  } catch (e) { /* backend offline */ }
40
  }
@@ -59,9 +123,9 @@ document.addEventListener('DOMContentLoaded', () => {
59
 
60
  if (meta.completionOnly) {
61
  if (verdict === true) {
62
- badge.textContent = 'PASS';
63
- badge.style.background = '#00ffaa33';
64
- badge.style.color = '#00ffaa';
65
  } else {
66
  badge.textContent = '';
67
  }
@@ -77,10 +141,9 @@ document.addEventListener('DOMContentLoaded', () => {
77
  badge.style.background = '#ff444433';
78
  badge.style.color = '#ff4444';
79
  } else if (completed === true) {
80
- // Ran stage artifact exists but no explicit pass/fail yet
81
- badge.textContent = 'PASS';
82
- badge.style.background = '#00ffaa33';
83
- badge.style.color = '#00ffaa';
84
  } else {
85
  badge.textContent = '';
86
  }
@@ -126,7 +189,7 @@ document.addEventListener('DOMContentLoaded', () => {
126
  const currentStage = document.querySelector('.nav-btn.active')?.dataset.stage;
127
  const meta = STAGE_METADATA[currentStage];
128
  if (meta && meta.gateKey && pipelineState[meta.gateKey] === false) {
129
- alert('Launch gate not passed. Complete the quality gate first.');
130
  return;
131
  }
132
  switchStage(nextBtn.dataset.next);
@@ -175,11 +238,17 @@ document.addEventListener('DOMContentLoaded', () => {
175
  source_path: datasetSourcePath.value
176
  })
177
  });
 
178
  const data = await res.json();
179
- alert('Configuration saved & verified successfully!');
 
 
 
 
 
180
  } catch (e) {
181
  console.error(e);
182
- alert('Failed to save dataset configuration.');
183
  }
184
  });
185
 
@@ -188,9 +257,11 @@ document.addEventListener('DOMContentLoaded', () => {
188
  try {
189
  const res = await fetch(`${API_BASE}/api/precheck`);
190
  const data = await res.json();
191
- alert(`Pre-Check Complete!\n\nStatus: ${data.status}\nDetails: ${data.message}`);
 
 
192
  } catch (e) {
193
- alert('Failed to complete pre-check.');
194
  }
195
  });
196
 
@@ -238,8 +309,19 @@ document.addEventListener('DOMContentLoaded', () => {
238
  })
239
  });
240
  const data = await res.json();
241
-
242
- // Start polling logs and status
 
 
 
 
 
 
 
 
 
 
 
243
  pollTraining();
244
  trainInterval = setInterval(pollTraining, 1500);
245
  } catch (e) {
@@ -289,7 +371,23 @@ document.addEventListener('DOMContentLoaded', () => {
289
  }
290
 
291
  if (!status.running) {
292
- terminalLogs.textContent += '\n--- Process finished ---\n';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  resetTrainingUI();
294
  }
295
  } catch (e) {
@@ -398,8 +496,17 @@ document.addEventListener('DOMContentLoaded', () => {
398
  })
399
  });
400
  const data = await res.json();
401
-
402
- // Populate select dropdown
 
 
 
 
 
 
 
 
 
403
  selectInferImage.innerHTML = '';
404
  if (data.images && data.images.length > 0) {
405
  data.images.forEach(img => {
@@ -408,15 +515,37 @@ document.addEventListener('DOMContentLoaded', () => {
408
  opt.textContent = img;
409
  selectInferImage.appendChild(opt);
410
  });
411
- // Trigger image load
412
  loadImage(data.images[0]);
413
  } else {
414
  selectInferImage.innerHTML = '<option value="">No images found</option>';
415
  }
416
- alert('Inference finished successfully!');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  } catch (e) {
418
  console.error(e);
419
- alert('Inference execution failed.');
420
  } finally {
421
  btnRunInfer.textContent = '⚡ Run Auto-Labeler';
422
  btnRunInfer.disabled = false;
@@ -447,14 +576,24 @@ document.addEventListener('DOMContentLoaded', () => {
447
  try {
448
  const res = await fetch(`${API_BASE}/api/grade`);
449
  const data = await res.json();
 
 
 
 
 
 
 
 
 
450
 
451
- graderScore.textContent = data.quality_score.toFixed(1);
452
- statTotalImgs.textContent = data.total_images;
453
- statTotalPreds.textContent = data.total_predictions;
454
 
455
- // Render issues list
456
  graderIssuesList.innerHTML = '';
457
- if (data.issues && data.issues.length > 0) {
 
 
458
  data.issues.forEach(issue => {
459
  const el = document.createElement('div');
460
  el.className = `issue-card ${issue.severity.toLowerCase()}`;
@@ -466,11 +605,23 @@ document.addEventListener('DOMContentLoaded', () => {
466
  graderIssuesList.appendChild(el);
467
  });
468
  } else {
469
- graderIssuesList.innerHTML = '<div class="empty-state">✅ Perfect score! No quality anomalies detected.</div>';
470
  }
 
 
 
 
 
 
 
 
 
 
 
 
471
  } catch (e) {
472
  console.error(e);
473
- alert('Failed to run quality diagnostics.');
474
  } finally {
475
  btnRunGrader.textContent = 'Run Quality Diagnostics';
476
  btnRunGrader.disabled = false;
@@ -617,11 +768,11 @@ document.addEventListener('DOMContentLoaded', () => {
617
  currentMitlData = data.data;
618
  originalCocText = currentMitlData.coc_trace;
619
  renderMitlData();
620
- alert(`Dataset loaded successfully: ${currentMitlData.dataset}`);
621
  }
622
  } catch (e) {
623
  console.error(e);
624
- alert('Failed to load NVIDIA dataset.');
625
  }
626
  }
627
 
@@ -758,7 +909,9 @@ document.addEventListener('DOMContentLoaded', () => {
758
  }
759
  } catch (e) {
760
  console.error(e);
761
- alert('Catalog metadata load failed.');
 
 
762
  } finally {
763
  btnPreprocessDataset.textContent = 'Load Catalog Metadata';
764
  btnPreprocessDataset.disabled = false;
@@ -839,12 +992,15 @@ document.addEventListener('DOMContentLoaded', () => {
839
  });
840
  const data = await res.json();
841
  if (data.status === 'ok') {
842
- alert(`Pipeline configuration saved!\n\n- Annotation: ${annotationTool}\n- Training: ${trainingFramework}\n- Validation: ${validationMethod}`);
 
 
 
843
  pipelineWizardModal.classList.add('hidden');
844
  }
845
  } catch (e) {
846
  console.error(e);
847
- alert('Failed to save pipeline configuration settings.');
848
  } finally {
849
  btnWizardSubmit.textContent = 'Save Pipeline Configuration';
850
  btnWizardSubmit.disabled = false;
@@ -947,7 +1103,7 @@ document.addEventListener('DOMContentLoaded', () => {
947
  // Add Box Handler
948
  btnMitlAddBox.addEventListener('click', () => {
949
  if (!currentMitlData) {
950
- alert('Please load a dataset first.');
951
  return;
952
  }
953
  const nextId = currentMitlData.annotations.length > 0
@@ -966,7 +1122,7 @@ document.addEventListener('DOMContentLoaded', () => {
966
  // Save Human Edits
967
  btnMitlSave.addEventListener('click', async () => {
968
  if (!currentMitlData) {
969
- alert('No dataset currently loaded to save.');
970
  return;
971
  }
972
 
@@ -994,7 +1150,7 @@ document.addEventListener('DOMContentLoaded', () => {
994
  });
995
 
996
  if (parseError) {
997
- alert('Invalid bounding box format. Please enter coordinates in format [x, y, w, h].');
998
  return;
999
  }
1000
 
@@ -1014,18 +1170,18 @@ document.addEventListener('DOMContentLoaded', () => {
1014
  // Update local memory
1015
  currentMitlData.annotations = updatedAnns;
1016
  currentMitlData.coc_trace = mitlCocTrace.value;
1017
- alert('Triage results successfully saved to runs/mitl_annotations.json!');
1018
  }
1019
  } catch (e) {
1020
  console.error(e);
1021
- alert('Failed to save human edits.');
1022
  }
1023
  });
1024
 
1025
  // Ask Gemma 4 critique
1026
  btnMitlAudit.addEventListener('click', async () => {
1027
  if (!currentMitlData) {
1028
- alert('No dataset currently loaded to audit.');
1029
  return;
1030
  }
1031
 
@@ -1312,7 +1468,7 @@ document.addEventListener('DOMContentLoaded', () => {
1312
  }
1313
  } catch (e) {
1314
  console.error(e);
1315
- alert("Failed to toggle MCP server state.");
1316
  checkbox.checked = !checkbox.checked;
1317
  bullet.style.transform = checkbox.checked ? 'translateX(22px)' : 'none';
1318
  updateSliderStyle();
@@ -1827,9 +1983,9 @@ document.addEventListener('DOMContentLoaded', () => {
1827
  }),
1828
  });
1829
  ssamState.selectedRow.manual_annotation = ssamDrawerAnnotation.value;
1830
- alert(`Annotation saved for ${ssamState.selectedRow.street_name}`);
1831
  } catch (e) {
1832
- alert('Failed to save annotation.');
1833
  } finally {
1834
  ssamDrawerSave.textContent = 'Save Annotation';
1835
  ssamDrawerSave.disabled = false;
@@ -2077,6 +2233,7 @@ document.addEventListener('DOMContentLoaded', () => {
2077
  const statusEl = document.getElementById('perception-status');
2078
  statusEl.textContent = 'Running SAM auto-label...';
2079
  try {
 
2080
  const res = await fetch(`${API_BASE}/api/perception/auto-label`, {
2081
  method: 'POST',
2082
  headers: { 'Content-Type': 'application/json' },
@@ -2084,17 +2241,63 @@ document.addEventListener('DOMContentLoaded', () => {
2084
  sequence_id: currentSequenceId,
2085
  sam_checkpoint: document.getElementById('perception-sam-checkpoint')?.value || 'models/sam_vit_b.pth',
2086
  device: document.getElementById('perception-device')?.value || 'cpu',
2087
- no_sam: true,
2088
  }),
2089
  });
2090
  const data = await res.json();
2091
- if (data.status === 'ok') {
2092
- const stubPrefix = data.demo_stub ? 'demo stub: ' : '';
2093
- const expected = data.frames_expected != null ? `/${data.frames_expected}` : '';
2094
- statusEl.textContent =
2095
- `${stubPrefix}Processed ${data.frames_processed}${expected} frames -> ${data.proposals_dir}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2096
  } else {
2097
- statusEl.textContent = `Error: ${data.detail || JSON.stringify(data)}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2098
  }
2099
  await refreshPipelineStatus();
2100
  await refreshAllOutputBrowsers();
@@ -2328,4 +2531,247 @@ document.addEventListener('DOMContentLoaded', () => {
2328
  }
2329
  });
2330
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2331
  });
 
2
  // API base URL
3
  const API_BASE = '';
4
 
5
+ // Non-blocking status signaling (replaces window.alert for routine feedback)
6
+ const toastStack = document.getElementById('toast-stack');
7
+ const TOAST_ICONS = { success: '✓', error: '✕', warning: '!', info: 'ℹ' };
8
+ const TOAST_DEFAULT_MS = { success: 3800, info: 4200, warning: 5200, error: 6500 };
9
+
10
+ function notify(message, type = 'info', options = {}) {
11
+ if (!toastStack || message == null || message === '') return;
12
+ const kind = ['success', 'error', 'warning', 'info'].includes(type) ? type : 'info';
13
+ const duration = options.duration ?? TOAST_DEFAULT_MS[kind];
14
+
15
+ const el = document.createElement('div');
16
+ el.className = `toast toast-${kind}`;
17
+ el.setAttribute('role', kind === 'error' ? 'alert' : 'status');
18
+
19
+ const icon = document.createElement('span');
20
+ icon.className = 'toast-icon';
21
+ icon.textContent = TOAST_ICONS[kind];
22
+ icon.setAttribute('aria-hidden', 'true');
23
+
24
+ const body = document.createElement('div');
25
+ body.className = 'toast-body';
26
+ body.textContent = String(message);
27
+
28
+ const close = document.createElement('button');
29
+ close.type = 'button';
30
+ close.className = 'toast-close';
31
+ close.setAttribute('aria-label', 'Dismiss');
32
+ close.textContent = '×';
33
+
34
+ let hideTimer = null;
35
+ const dismiss = () => {
36
+ if (hideTimer) clearTimeout(hideTimer);
37
+ el.classList.add('toast-out');
38
+ setTimeout(() => el.remove(), 180);
39
+ };
40
+ close.addEventListener('click', dismiss);
41
+
42
+ el.appendChild(icon);
43
+ el.appendChild(body);
44
+ el.appendChild(close);
45
+ toastStack.appendChild(el);
46
+
47
+ if (duration > 0) {
48
+ hideTimer = setTimeout(dismiss, duration);
49
+ }
50
+ return dismiss;
51
+ }
52
+
53
  // Stage Navigation
54
  const navButtons = document.querySelectorAll('.nav-btn');
55
  const panels = document.querySelectorAll('.stage-panel');
 
78
  let pipelineState = {};
79
  let currentSequenceId = 'seq_001';
80
 
81
+ async function syncActiveSequence() {
82
+ try {
83
+ const cfg = await fetch(`${API_BASE}/api/config`).then((r) => r.json());
84
+ if (cfg?.sequence_id) {
85
+ currentSequenceId = cfg.sequence_id;
86
+ return;
87
+ }
88
+ } catch (e) { /* ignore */ }
89
+ try {
90
+ const st = await fetch(`${API_BASE}/api/pipeline/status?sequence_id=${encodeURIComponent(currentSequenceId)}`).then((r) => r.json());
91
+ if (st?.active_sequence_id) currentSequenceId = st.active_sequence_id;
92
+ } catch (e) { /* ignore */ }
93
+ }
94
+
95
  async function refreshPipelineStatus() {
96
  try {
97
+ await syncActiveSequence();
98
+ const res = await fetch(`${API_BASE}/api/pipeline/status?sequence_id=${encodeURIComponent(currentSequenceId)}`);
99
  const data = await res.json();
100
  pipelineState = data;
101
+ if (data.active_sequence_id) currentSequenceId = data.active_sequence_id;
102
  updateGateBadges();
103
  } catch (e) { /* backend offline */ }
104
  }
 
123
 
124
  if (meta.completionOnly) {
125
  if (verdict === true) {
126
+ badge.textContent = pipelineState.demo_stub ? 'STUB' : 'DONE';
127
+ badge.style.background = pipelineState.demo_stub ? '#fbbf2433' : '#00ffaa33';
128
+ badge.style.color = pipelineState.demo_stub ? '#fbbf24' : '#00ffaa';
129
  } else {
130
  badge.textContent = '';
131
  }
 
141
  badge.style.background = '#ff444433';
142
  badge.style.color = '#ff4444';
143
  } else if (completed === true) {
144
+ badge.textContent = 'RAN';
145
+ badge.style.background = '#3b82f633';
146
+ badge.style.color = '#93c5fd';
 
147
  } else {
148
  badge.textContent = '';
149
  }
 
189
  const currentStage = document.querySelector('.nav-btn.active')?.dataset.stage;
190
  const meta = STAGE_METADATA[currentStage];
191
  if (meta && meta.gateKey && pipelineState[meta.gateKey] === false) {
192
+ notify('Launch gate not passed. Complete the quality gate first.', 'warning');
193
  return;
194
  }
195
  switchStage(nextBtn.dataset.next);
 
238
  source_path: datasetSourcePath.value
239
  })
240
  });
241
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
242
  const data = await res.json();
243
+ notify('Configuration saved (YAML/paths not yet executed).', 'success');
244
+ const cfgStatus = document.getElementById('config-exec-status');
245
+ if (cfgStatus) {
246
+ cfgStatus.textContent = 'CONFIGURATION SAVED / not yet used by an execution — run Load & Preprocess.';
247
+ cfgStatus.dataset.modified = '1';
248
+ }
249
  } catch (e) {
250
  console.error(e);
251
+ notify('Failed to save dataset configuration.', 'error');
252
  }
253
  });
254
 
 
257
  try {
258
  const res = await fetch(`${API_BASE}/api/precheck`);
259
  const data = await res.json();
260
+ const precheckType = data.verified ? 'success' : 'warning';
261
+ notify(`Pre-check: ${data.status}\n${data.message || ''}`, precheckType);
262
+ refreshScriptStatus(data.scripts);
263
  } catch (e) {
264
+ notify('Failed to complete pre-check.', 'error');
265
  }
266
  });
267
 
 
309
  })
310
  });
311
  const data = await res.json();
312
+ if (!res.ok) {
313
+ terminalLogs.textContent += `Error: ${JSON.stringify(data)}\n`;
314
+ resetTrainingUI();
315
+ notify(data.detail?.message || data.detail || 'Failed to start training', 'error');
316
+ return;
317
+ }
318
+ renderEvidenceCard('evidence-training', {
319
+ status: 'RUNNING',
320
+ execution_id: data.execution_id,
321
+ process_id: data.process_id,
322
+ command: data.command,
323
+ message: 'Training process spawned',
324
+ });
325
  pollTraining();
326
  trainInterval = setInterval(pollTraining, 1500);
327
  } catch (e) {
 
371
  }
372
 
373
  if (!status.running) {
374
+ const verdict = status.status || (status.exit_code === 0 ? 'SUCCEEDED' : 'FAILED');
375
+ terminalLogs.textContent += `\n--- Process finished: ${verdict} exit_code=${status.exit_code} ---\n`;
376
+ renderEvidenceCard('evidence-training', {
377
+ status: verdict,
378
+ execution_id: status.execution_id,
379
+ exit_code: status.exit_code,
380
+ process_id: status.process_id,
381
+ command: status.command,
382
+ metrics: {
383
+ epochs_observed: status.epochs_observed,
384
+ epochs_requested: status.epochs_requested,
385
+ checkpoint: status.checkpoint,
386
+ losses_parsed: (status.losses || []).length,
387
+ },
388
+ message: `Training ${verdict}`,
389
+ });
390
+ refreshExecutionConsole();
391
  resetTrainingUI();
392
  }
393
  } catch (e) {
 
496
  })
497
  });
498
  const data = await res.json();
499
+ if (!res.ok) {
500
+ const detail = data.detail?.message || data.detail || JSON.stringify(data);
501
+ notify(`Inference failed: ${detail}`, 'error');
502
+ renderEvidenceCard('evidence-inference', {
503
+ status: 'FAILED',
504
+ execution_id: data.detail?.execution_id || data.execution_id,
505
+ message: detail,
506
+ });
507
+ return;
508
+ }
509
+
510
  selectInferImage.innerHTML = '';
511
  if (data.images && data.images.length > 0) {
512
  data.images.forEach(img => {
 
515
  opt.textContent = img;
516
  selectInferImage.appendChild(opt);
517
  });
 
518
  loadImage(data.images[0]);
519
  } else {
520
  selectInferImage.innerHTML = '<option value="">No images found</option>';
521
  }
522
+ renderEvidenceCard('evidence-inference', {
523
+ status: data.status,
524
+ execution_id: data.execution_id,
525
+ duration_ms: data.duration_ms,
526
+ model: data.model,
527
+ checkpoint: data.checkpoint,
528
+ metrics: {
529
+ discovered: data.records_discovered,
530
+ processed: data.records_processed,
531
+ succeeded: data.records_succeeded,
532
+ failed: data.records_failed,
533
+ inference_calls: data.inference_calls,
534
+ predictions: data.predictions_generated,
535
+ output_dir: data.output_dir,
536
+ exit_code: data.exit_code,
537
+ },
538
+ message: data.message,
539
+ });
540
+ refreshExecutionConsole();
541
+ if (data.status === 'SUCCEEDED' || data.status === 'PARTIAL_SUCCESS') {
542
+ notify(`Inference ${data.status}: ${data.records_succeeded}/${data.records_discovered} images`, 'success');
543
+ } else {
544
+ notify(`Inference ${data.status}: ${data.message || 'see evidence'}`, 'warning');
545
+ }
546
  } catch (e) {
547
  console.error(e);
548
+ notify('Inference execution failed.', 'error');
549
  } finally {
550
  btnRunInfer.textContent = '⚡ Run Auto-Labeler';
551
  btnRunInfer.disabled = false;
 
576
  try {
577
  const res = await fetch(`${API_BASE}/api/grade`);
578
  const data = await res.json();
579
+ if (!res.ok) {
580
+ notify(data.detail?.message || data.detail || 'Grader failed', 'error');
581
+ renderEvidenceCard('evidence-grader', {
582
+ status: 'FAILED',
583
+ execution_id: data.detail?.execution_id || data.execution_id,
584
+ message: data.detail?.message || data.detail,
585
+ });
586
+ return;
587
+ }
588
 
589
+ graderScore.textContent = data.quality_score != null ? Number(data.quality_score).toFixed(1) : '—';
590
+ statTotalImgs.textContent = data.total_images ?? '—';
591
+ statTotalPreds.textContent = data.total_predictions ?? '—';
592
 
 
593
  graderIssuesList.innerHTML = '';
594
+ if (data.status === 'NOT_EXECUTED') {
595
+ graderIssuesList.innerHTML = '<div class="empty-state">NOT_EXECUTED — no predictions artifact. This is not a perfect score.</div>';
596
+ } else if (data.issues && data.issues.length > 0) {
597
  data.issues.forEach(issue => {
598
  const el = document.createElement('div');
599
  el.className = `issue-card ${issue.severity.toLowerCase()}`;
 
605
  graderIssuesList.appendChild(el);
606
  });
607
  } else {
608
+ graderIssuesList.innerHTML = '<div class="empty-state">No quality anomalies reported by grader output (not a claim of ground-truth perfection).</div>';
609
  }
610
+ renderEvidenceCard('evidence-grader', {
611
+ status: data.status,
612
+ execution_id: data.execution_id,
613
+ duration_ms: data.duration_ms,
614
+ metrics: data.metrics || {
615
+ total_images: data.total_images,
616
+ total_predictions: data.total_predictions,
617
+ quality_score: data.quality_score,
618
+ },
619
+ message: data.message,
620
+ });
621
+ refreshExecutionConsole();
622
  } catch (e) {
623
  console.error(e);
624
+ notify('Failed to run quality diagnostics.', 'error');
625
  } finally {
626
  btnRunGrader.textContent = 'Run Quality Diagnostics';
627
  btnRunGrader.disabled = false;
 
768
  currentMitlData = data.data;
769
  originalCocText = currentMitlData.coc_trace;
770
  renderMitlData();
771
+ notify(`Dataset loaded: ${currentMitlData.dataset}`, 'success');
772
  }
773
  } catch (e) {
774
  console.error(e);
775
+ notify('Failed to load NVIDIA dataset.', 'error');
776
  }
777
  }
778
 
 
909
  }
910
  } catch (e) {
911
  console.error(e);
912
+ const statusEl = document.getElementById('source-browse-status');
913
+ if (statusEl) statusEl.textContent = 'Catalog metadata load failed.';
914
+ notify('Catalog metadata load failed.', 'error');
915
  } finally {
916
  btnPreprocessDataset.textContent = 'Load Catalog Metadata';
917
  btnPreprocessDataset.disabled = false;
 
992
  });
993
  const data = await res.json();
994
  if (data.status === 'ok') {
995
+ notify(
996
+ `Pipeline configuration saved\nAnnotation: ${annotationTool}\nTraining: ${trainingFramework}\nValidation: ${validationMethod}`,
997
+ 'success'
998
+ );
999
  pipelineWizardModal.classList.add('hidden');
1000
  }
1001
  } catch (e) {
1002
  console.error(e);
1003
+ notify('Failed to save pipeline configuration settings.', 'error');
1004
  } finally {
1005
  btnWizardSubmit.textContent = 'Save Pipeline Configuration';
1006
  btnWizardSubmit.disabled = false;
 
1103
  // Add Box Handler
1104
  btnMitlAddBox.addEventListener('click', () => {
1105
  if (!currentMitlData) {
1106
+ notify('Please load a dataset first.', 'warning');
1107
  return;
1108
  }
1109
  const nextId = currentMitlData.annotations.length > 0
 
1122
  // Save Human Edits
1123
  btnMitlSave.addEventListener('click', async () => {
1124
  if (!currentMitlData) {
1125
+ notify('No dataset currently loaded to save.', 'warning');
1126
  return;
1127
  }
1128
 
 
1150
  });
1151
 
1152
  if (parseError) {
1153
+ notify('Invalid bounding box format. Use coordinates [x, y, w, h].', 'error');
1154
  return;
1155
  }
1156
 
 
1170
  // Update local memory
1171
  currentMitlData.annotations = updatedAnns;
1172
  currentMitlData.coc_trace = mitlCocTrace.value;
1173
+ notify('Triage results saved to runs/mitl_annotations.json.', 'success');
1174
  }
1175
  } catch (e) {
1176
  console.error(e);
1177
+ notify('Failed to save human edits.', 'error');
1178
  }
1179
  });
1180
 
1181
  // Ask Gemma 4 critique
1182
  btnMitlAudit.addEventListener('click', async () => {
1183
  if (!currentMitlData) {
1184
+ notify('No dataset currently loaded to audit.', 'warning');
1185
  return;
1186
  }
1187
 
 
1468
  }
1469
  } catch (e) {
1470
  console.error(e);
1471
+ notify('Failed to toggle MCP server state.', 'error');
1472
  checkbox.checked = !checkbox.checked;
1473
  bullet.style.transform = checkbox.checked ? 'translateX(22px)' : 'none';
1474
  updateSliderStyle();
 
1983
  }),
1984
  });
1985
  ssamState.selectedRow.manual_annotation = ssamDrawerAnnotation.value;
1986
+ notify(`Annotation saved for ${ssamState.selectedRow.street_name}`, 'success');
1987
  } catch (e) {
1988
+ notify('Failed to save annotation.', 'error');
1989
  } finally {
1990
  ssamDrawerSave.textContent = 'Save Annotation';
1991
  ssamDrawerSave.disabled = false;
 
2233
  const statusEl = document.getElementById('perception-status');
2234
  statusEl.textContent = 'Running SAM auto-label...';
2235
  try {
2236
+ await syncActiveSequence();
2237
  const res = await fetch(`${API_BASE}/api/perception/auto-label`, {
2238
  method: 'POST',
2239
  headers: { 'Content-Type': 'application/json' },
 
2241
  sequence_id: currentSequenceId,
2242
  sam_checkpoint: document.getElementById('perception-sam-checkpoint')?.value || 'models/sam_vit_b.pth',
2243
  device: document.getElementById('perception-device')?.value || 'cpu',
2244
+ no_sam: !!document.getElementById('perception-no-sam')?.checked,
2245
  }),
2246
  });
2247
  const data = await res.json();
2248
+ const payload = (data && typeof data.detail === 'object') ? data.detail : data;
2249
+ const status = payload.status || (res.ok ? 'SUCCEEDED' : 'FAILED');
2250
+ const nextSteps = payload.next_steps || [];
2251
+ const stubPrefix = payload.demo_stub ? 'demo stub · ' : '';
2252
+
2253
+ if (!res.ok || status === 'FAILED') {
2254
+ statusEl.textContent = [
2255
+ `${status}: ${payload.message || 'Auto-label could not run.'}`,
2256
+ payload.checkpoint?.exists === false ? 'SAM checkpoint missing.' : '',
2257
+ payload.frames_expected === 0 ? 'No ingested frames for this sequence.' : '',
2258
+ nextSteps.length ? `Next: ${nextSteps[0]}` : '',
2259
+ ].filter(Boolean).join('\n');
2260
+ renderEvidenceCard('evidence-perception', {
2261
+ status: 'FAILED',
2262
+ execution_id: payload.execution_id,
2263
+ duration_ms: payload.duration_ms,
2264
+ model: payload.model,
2265
+ checkpoint: payload.checkpoint,
2266
+ metrics: {
2267
+ frames_processed: payload.frames_processed,
2268
+ frames_expected: payload.frames_expected,
2269
+ predictions_generated: payload.predictions_generated,
2270
+ sam_ran: payload.sam_ran,
2271
+ },
2272
+ message: payload.message,
2273
+ next_steps: nextSteps,
2274
+ events: payload.events,
2275
+ });
2276
  } else {
2277
+ statusEl.textContent = [
2278
+ `${status}: ${stubPrefix}${payload.frames_processed}/${payload.frames_expected} frames`,
2279
+ payload.message || '',
2280
+ nextSteps.length && status !== 'SUCCEEDED' ? `Next: ${nextSteps[0]}` : '',
2281
+ ].filter(Boolean).join('\n');
2282
+ renderEvidenceCard('evidence-perception', {
2283
+ status,
2284
+ execution_id: payload.execution_id,
2285
+ duration_ms: payload.duration_ms,
2286
+ model: payload.model,
2287
+ checkpoint: payload.checkpoint,
2288
+ metrics: {
2289
+ frames_processed: payload.frames_processed,
2290
+ frames_expected: payload.frames_expected,
2291
+ predictions_generated: payload.predictions_generated,
2292
+ inference_calls: payload.inference_calls,
2293
+ sam_ran: payload.sam_ran,
2294
+ output_dir: payload.proposals_dir,
2295
+ },
2296
+ message: payload.message,
2297
+ next_steps: nextSteps,
2298
+ events: payload.events,
2299
+ });
2300
+ refreshExecutionConsole();
2301
  }
2302
  await refreshPipelineStatus();
2303
  await refreshAllOutputBrowsers();
 
2531
  }
2532
  });
2533
  }
2534
+
2535
+ // --- Execution integrity: evidence cards, console, strict mode, load ---
2536
+ function esc(s) {
2537
+ return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2538
+ }
2539
+
2540
+ function renderEvidenceCard(elementId, evidence) {
2541
+ const el = document.getElementById(elementId);
2542
+ if (!el || !evidence) return;
2543
+ const status = evidence.status || 'NOT_EXECUTED';
2544
+ const metrics = evidence.metrics || {};
2545
+ const metricRows = Object.entries(metrics).map(([k, v]) => {
2546
+ const val = typeof v === 'object' ? JSON.stringify(v) : v;
2547
+ return `<div class="ev-row"><span>${esc(k)}</span><strong>${esc(val)}</strong></div>`;
2548
+ }).join('');
2549
+ const events = (evidence.events || []).slice(-8).map(ev =>
2550
+ `<li><code>${esc(ev.ts || '')}</code> ${esc(ev.kind || '')}: ${esc(ev.message || '')}</li>`
2551
+ ).join('');
2552
+ const ckpt = evidence.checkpoint
2553
+ ? `<div class="ev-row"><span>checkpoint</span><strong>${esc(evidence.checkpoint.path || evidence.checkpoint)} ${evidence.checkpoint.exists === false ? '(missing)' : ''}</strong></div>`
2554
+ : '';
2555
+ const nextSteps = (evidence.next_steps || []).map((s) => `<li>${esc(s)}</li>`).join('');
2556
+ el.innerHTML = `
2557
+ <h4>What actually happened?</h4>
2558
+ <div class="ev-status ev-status-${esc(status)}">${esc(status)}</div>
2559
+ <div class="ev-row"><span>execution_id</span><strong class="font-mono">${esc(evidence.execution_id || '—')}</strong></div>
2560
+ ${evidence.duration_ms != null ? `<div class="ev-row"><span>duration_ms</span><strong>${esc(evidence.duration_ms)}</strong></div>` : ''}
2561
+ ${evidence.model ? `<div class="ev-row"><span>model</span><strong>${esc(evidence.model)}</strong></div>` : ''}
2562
+ ${ckpt}
2563
+ ${evidence.process_id != null ? `<div class="ev-row"><span>process_id</span><strong>${esc(evidence.process_id)}</strong></div>` : ''}
2564
+ ${evidence.exit_code != null ? `<div class="ev-row"><span>exit_code</span><strong>${esc(evidence.exit_code)}</strong></div>` : ''}
2565
+ ${evidence.command ? `<div class="ev-row"><span>command</span><strong class="font-mono">${esc(Array.isArray(evidence.command) ? evidence.command.join(' ') : evidence.command)}</strong></div>` : ''}
2566
+ ${metricRows}
2567
+ ${evidence.message ? `<p class="ev-msg">${esc(evidence.message)}</p>` : ''}
2568
+ ${nextSteps ? `<div class="ev-next"><strong>What to do next</strong><ul>${nextSteps}</ul></div>` : ''}
2569
+ ${evidence.execution_id ? `<a class="ev-log-link" href="/api/executions/${encodeURIComponent(evidence.execution_id)}/log" target="_blank" rel="noopener">Open execution log</a>` : ''}
2570
+ ${events ? `<details class="ev-timeline"><summary>Event timeline</summary><ul>${events}</ul></details>` : ''}
2571
+ `;
2572
+ }
2573
+
2574
+ function refreshScriptStatus(scripts) {
2575
+ const map = {
2576
+ 'train.py': 'script-status-train',
2577
+ 'infer.py': 'script-status-infer',
2578
+ 'autograder.py': 'script-status-grader',
2579
+ };
2580
+ Object.entries(map).forEach(([name, id]) => {
2581
+ const el = document.getElementById(id);
2582
+ const r = scripts?.[name];
2583
+ if (!el || !r) return;
2584
+ if (!r.exists) {
2585
+ el.textContent = `Missing (${name})`;
2586
+ el.className = 'val font-mono';
2587
+ el.style.color = '#ff8888';
2588
+ } else if (r.syntax_valid && r.dry_run_ok) {
2589
+ el.textContent = `Verified (${name})${r.last_status ? ' · last ' + r.last_status : ''}`;
2590
+ el.className = 'val font-mono green';
2591
+ el.style.color = '';
2592
+ } else {
2593
+ el.textContent = `Found but unverified (${name})`;
2594
+ el.className = 'val font-mono';
2595
+ el.style.color = '#fbbf24';
2596
+ }
2597
+ });
2598
+ }
2599
+
2600
+ async function refreshHealth() {
2601
+ try {
2602
+ const res = await fetch(`${API_BASE}/api/health`);
2603
+ const data = await res.json();
2604
+ const chip = document.getElementById('health-status-chip');
2605
+ if (chip) {
2606
+ chip.textContent = [
2607
+ data.filesystem_writable ? 'FS writable' : 'FS FAIL',
2608
+ data.torch_installed ? 'torch' : 'no-torch',
2609
+ data.ultralytics_installed ? 'yolo' : 'no-yolo',
2610
+ data.gpu?.available ? `GPU:${data.gpu.name}` : 'CPU',
2611
+ data.last_successful_execution_id ? `last:${data.last_successful_execution_id}` : 'no-success-yet',
2612
+ ].join(' · ');
2613
+ }
2614
+ if (data.scripts) refreshScriptStatus(data.scripts);
2615
+ const tog = document.getElementById('toggle-strict-mode');
2616
+ if (tog) tog.checked = !!data.strict_mode;
2617
+ } catch (e) {
2618
+ const chip = document.getElementById('health-status-chip');
2619
+ if (chip) chip.textContent = 'Backend unreachable';
2620
+ }
2621
+ }
2622
+
2623
+ async function refreshExecutionConsole() {
2624
+ const list = document.getElementById('exec-console-list');
2625
+ if (!list) return;
2626
+ try {
2627
+ const res = await fetch(`${API_BASE}/api/executions?limit=40`);
2628
+ const data = await res.json();
2629
+ const rows = data.executions || [];
2630
+ if (!rows.length) {
2631
+ list.innerHTML = '<p class="evidence-empty">No executions recorded yet.</p>';
2632
+ return;
2633
+ }
2634
+ list.innerHTML = rows.map(e => `
2635
+ <button type="button" class="exec-row" data-id="${esc(e.execution_id)}">
2636
+ <span class="ev-status ev-status-${esc(e.status)}">${esc(e.status)}</span>
2637
+ <span class="font-mono">${esc(e.execution_id)}</span>
2638
+ <span>${esc(e.operation)}</span>
2639
+ <span>${esc(e.duration_ms != null ? e.duration_ms + 'ms' : '—')}</span>
2640
+ <span>${e.verified ? 'verified' : 'unverified'}</span>
2641
+ </button>
2642
+ `).join('');
2643
+ list.querySelectorAll('.exec-row').forEach(btn => {
2644
+ btn.addEventListener('click', async () => {
2645
+ const detail = document.getElementById('exec-console-detail');
2646
+ const r = await fetch(`${API_BASE}/api/executions/${encodeURIComponent(btn.dataset.id)}`);
2647
+ const full = await r.json();
2648
+ detail.classList.remove('hidden');
2649
+ detail.innerHTML = `<pre>${esc(JSON.stringify(full, null, 2))}</pre>`;
2650
+ });
2651
+ });
2652
+ } catch (e) {
2653
+ list.innerHTML = `<p class="evidence-empty">Failed to load executions: ${esc(e.message)}</p>`;
2654
+ }
2655
+ }
2656
+
2657
+ const btnLoadPreprocess = document.getElementById('btn-load-preprocess');
2658
+ if (btnLoadPreprocess) {
2659
+ btnLoadPreprocess.addEventListener('click', async () => {
2660
+ btnLoadPreprocess.disabled = true;
2661
+ btnLoadPreprocess.textContent = 'Loading…';
2662
+ const statusEl = document.getElementById('source-browse-status');
2663
+ try {
2664
+ const res = await fetch(`${API_BASE}/api/dataset/load`, {
2665
+ method: 'POST',
2666
+ headers: { 'Content-Type': 'application/json' },
2667
+ body: JSON.stringify({
2668
+ source_path: datasetSourcePath?.value || 'data',
2669
+ yaml_path: dataYamlPath?.value || 'coco8.yaml',
2670
+ dataset_type: selectDatasetType?.value || 'local',
2671
+ }),
2672
+ });
2673
+ const data = await res.json();
2674
+ const m = data.metrics || {};
2675
+ if (statusEl) {
2676
+ statusEl.textContent = `${data.status}: discovered=${m.images_discovered ?? 0} readable=${m.images_readable ?? 0} corrupt=${m.images_corrupt ?? 0} → ${data.manifest_path || ''}`;
2677
+ }
2678
+ renderEvidenceCard('evidence-dataset', {
2679
+ status: data.status,
2680
+ execution_id: data.execution_id,
2681
+ duration_ms: data.duration_ms,
2682
+ metrics: {
2683
+ ...m,
2684
+ reconciliation: data.reconciliation?.state,
2685
+ manifest: data.manifest_path,
2686
+ },
2687
+ message: data.message,
2688
+ events: data.events,
2689
+ });
2690
+ const badge = document.getElementById('ds-meta-badge');
2691
+ const loadedEl = document.getElementById('ds-kpi-loaded-rows');
2692
+ const pctEl = document.getElementById('ds-kpi-ingest-pct');
2693
+ if (loadedEl) loadedEl.textContent = m.images_readable ?? 0;
2694
+ if (pctEl) pctEl.textContent = m.loaded_pct_of_discovered != null ? `${m.loaded_pct_of_discovered}% of discovered` : '—';
2695
+ if (badge) {
2696
+ if (data.status === 'FAILED' || (m.images_readable ?? 0) === 0) {
2697
+ badge.textContent = 'FAILED — 0 loaded';
2698
+ badge.style.background = 'rgba(255, 68, 68, 0.12)';
2699
+ badge.style.color = '#ff8888';
2700
+ } else {
2701
+ badge.textContent = `${m.images_readable} loaded (evidence)`;
2702
+ badge.style.background = 'rgba(0, 255, 170, 0.15)';
2703
+ badge.style.color = '#00ffaa';
2704
+ }
2705
+ }
2706
+ const cfgStatus = document.getElementById('config-exec-status');
2707
+ if (cfgStatus) {
2708
+ cfgStatus.textContent = `used_by_execution YES · last execution_id ${data.execution_id}`;
2709
+ cfgStatus.dataset.modified = '0';
2710
+ }
2711
+ refreshExecutionConsole();
2712
+ if (data.status === 'FAILED' || data.status === 'VALIDATION_FAILED') {
2713
+ notify(data.message || 'Load failed', 'error');
2714
+ } else if (data.status === 'PARTIAL_SUCCESS') {
2715
+ notify(data.message || 'Partial load', 'warning');
2716
+ } else {
2717
+ notify(data.message || 'Load complete', 'success');
2718
+ }
2719
+ } catch (e) {
2720
+ if (statusEl) statusEl.textContent = `Load failed: ${e.message}`;
2721
+ notify('Load & Preprocess failed.', 'error');
2722
+ } finally {
2723
+ btnLoadPreprocess.disabled = false;
2724
+ btnLoadPreprocess.textContent = 'Load & Preprocess';
2725
+ }
2726
+ });
2727
+ }
2728
+
2729
+ const btnYamlValidate = document.getElementById('btn-yaml-validate');
2730
+ if (btnYamlValidate) {
2731
+ btnYamlValidate.addEventListener('click', async () => {
2732
+ try {
2733
+ const res = await fetch(`${API_BASE}/api/yaml/validate`, {
2734
+ method: 'POST',
2735
+ headers: { 'Content-Type': 'application/json' },
2736
+ body: JSON.stringify({ path: dataYamlPath?.value || 'coco8.yaml' }),
2737
+ });
2738
+ const data = await res.json();
2739
+ notify(`${data.status}: classes=${data.class_count} images=${JSON.stringify(data.image_counts)}`, data.status === 'SUCCEEDED' ? 'success' : 'warning');
2740
+ const cfgStatus = document.getElementById('config-exec-status');
2741
+ if (cfgStatus) {
2742
+ cfgStatus.textContent = `YAML hash ${data.content_hash || '—'} · ${data.status} · execution ${data.execution_id || '—'}`;
2743
+ }
2744
+ refreshExecutionConsole();
2745
+ } catch (e) {
2746
+ notify(`YAML validate failed: ${e.message}`, 'error');
2747
+ }
2748
+ });
2749
+ }
2750
+
2751
+ document.getElementById('btn-open-exec-console')?.addEventListener('click', () => {
2752
+ document.getElementById('exec-console-modal')?.classList.remove('hidden');
2753
+ refreshExecutionConsole();
2754
+ });
2755
+ document.getElementById('btn-close-exec-console')?.addEventListener('click', () => {
2756
+ document.getElementById('exec-console-modal')?.classList.add('hidden');
2757
+ });
2758
+ document.getElementById('btn-refresh-executions')?.addEventListener('click', refreshExecutionConsole);
2759
+ document.getElementById('btn-health-check')?.addEventListener('click', async () => {
2760
+ await refreshHealth();
2761
+ notify('Health refreshed — see Workspace Status chip.', 'info');
2762
+ });
2763
+ document.getElementById('toggle-strict-mode')?.addEventListener('change', async (e) => {
2764
+ try {
2765
+ await fetch(`${API_BASE}/api/strict-mode`, {
2766
+ method: 'POST',
2767
+ headers: { 'Content-Type': 'application/json' },
2768
+ body: JSON.stringify({ enabled: e.target.checked }),
2769
+ });
2770
+ notify(e.target.checked ? 'Strict Execution Mode ON' : 'Strict Execution Mode OFF', 'info');
2771
+ } catch (err) {
2772
+ notify('Failed to toggle strict mode', 'error');
2773
+ }
2774
+ });
2775
+
2776
+ refreshHealth();
2777
  });
static/index.html CHANGED
@@ -102,7 +102,12 @@
102
  <h2 id="stage-title">Dataset Configuration</h2>
103
  <p id="stage-desc">Prepare and validate your image source path and annotation files.</p>
104
  </div>
105
- <div class="controls-area">
 
 
 
 
 
106
  <button class="btn btn-secondary" id="btn-validate">Run Pre-Check</button>
107
  <button class="btn btn-primary" id="btn-global-action">Proceed to Model</button>
108
  </div>
@@ -141,11 +146,18 @@
141
  </div>
142
  <div class="actions-row" style="display: flex; gap: 12px; flex-wrap: wrap;">
143
  <button class="btn btn-primary" id="btn-save-dataset">Save Config</button>
 
144
  <button class="btn btn-secondary" id="btn-preprocess-dataset">Load Catalog Metadata</button>
145
  <button class="btn btn-secondary" id="btn-browse-source" title="List and preview images under Images Path">Validate &amp; Browse Images Path</button>
 
146
  </div>
 
147
  <div id="source-browse-status" class="status-box" style="margin-top:12px;"></div>
148
  <div id="source-browse-gallery" class="output-browse-gallery" style="margin-top:12px;"></div>
 
 
 
 
149
  </div>
150
 
151
  <div class="card card-glass">
@@ -157,15 +169,19 @@
157
  </div>
158
  <div class="info-row">
159
  <span class="lbl">Train script:</span>
160
- <span class="val font-mono green">Found (train.py)</span>
161
  </div>
162
  <div class="info-row">
163
  <span class="lbl">Infer script:</span>
164
- <span class="val font-mono green">Found (infer.py)</span>
165
  </div>
166
  <div class="info-row">
167
  <span class="lbl">Auto-Grader script:</span>
168
- <span class="val font-mono green">Found (autograder.py)</span>
 
 
 
 
169
  </div>
170
  </div>
171
  </div>
@@ -346,7 +362,11 @@
346
  <p class="text-muted">Generate 2D SAM masks and lift to 3D bounding box proposals.</p>
347
  <div class="form-group">
348
  <label for="perception-sam-checkpoint">SAM Checkpoint</label>
349
- <input type="text" id="perception-sam-checkpoint" value="models/sam_vit_b.pth" class="input-field">
 
 
 
 
350
  </div>
351
  <div class="form-group">
352
  <label for="perception-device">Device</label>
@@ -355,8 +375,15 @@
355
  <option value="cuda">CUDA</option>
356
  </select>
357
  </div>
 
 
 
358
  <button class="btn btn-primary" id="btn-perception-run">Run Auto-Label</button>
359
  <div id="perception-status" class="status-box" style="margin-top:16px;"></div>
 
 
 
 
360
  </div>
361
  <div class="card card-glass pipeline-output-browser" style="margin-top:16px;" data-browser="perception">
362
  <h3>Pipeline Outputs — proposals &amp; frames</h3>
@@ -392,6 +419,10 @@
392
  <button class="btn btn-primary" id="btn-tracking-run">Run Temporal Tracker</button>
393
  <div id="tracking-status" class="status-box" style="margin-top:16px;"></div>
394
  <div id="tracking-tracks-table" style="margin-top:16px;"></div>
 
 
 
 
395
  </div>
396
  </section>
397
 
@@ -403,6 +434,10 @@
403
  <button class="btn btn-primary" id="btn-quality-gate-run">Run Quality Gate</button>
404
  <div id="quality-gate-status" class="status-box" style="margin-top:16px;"></div>
405
  <div id="quality-gate-metrics" style="margin-top:16px;"></div>
 
 
 
 
406
  </div>
407
  </section>
408
 
@@ -522,6 +557,10 @@ ollama run gemma4:26b
522
  <div class="progress-bar-fill" id="train-progress-bar" style="width: 0%"></div>
523
  </div>
524
  </div>
 
 
 
 
525
  </div>
526
 
527
  <div class="card card-glass">
@@ -562,6 +601,10 @@ ollama run gemma4:26b
562
  <span class="range-val" id="val-infer-iou">0.45</span>
563
  </div>
564
  <button class="btn btn-primary btn-large" id="btn-run-infer">⚡ Run Auto-Labeler</button>
 
 
 
 
565
  </div>
566
 
567
  <div class="card card-glass">
@@ -603,6 +646,10 @@ ollama run gemma4:26b
603
  </div>
604
  </div>
605
  <button class="btn btn-primary width-full" id="btn-run-grader" style="margin-top: 15px;">Run Quality Diagnostics</button>
 
 
 
 
606
  </div>
607
 
608
  <div class="card card-glass">
@@ -1071,6 +1118,23 @@ ollama run gemma4:26b
1071
  </div>
1072
  </div>
1073
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1074
  <script src="app.js"></script>
1075
  </body>
1076
  </html>
 
102
  <h2 id="stage-title">Dataset Configuration</h2>
103
  <p id="stage-desc">Prepare and validate your image source path and annotation files.</p>
104
  </div>
105
+ <div class="controls-area" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
106
+ <label class="strict-mode-toggle" title="Refuse SUCCEEDED unless process, exit, outputs, metrics, and execution_id are verified">
107
+ <input type="checkbox" id="toggle-strict-mode"> Strict Execution
108
+ </label>
109
+ <button class="btn btn-secondary" id="btn-open-exec-console" title="Global execution history">Execution Console</button>
110
+ <button class="btn btn-secondary" id="btn-health-check" title="Meaningful backend health">Health</button>
111
  <button class="btn btn-secondary" id="btn-validate">Run Pre-Check</button>
112
  <button class="btn btn-primary" id="btn-global-action">Proceed to Model</button>
113
  </div>
 
146
  </div>
147
  <div class="actions-row" style="display: flex; gap: 12px; flex-wrap: wrap;">
148
  <button class="btn btn-primary" id="btn-save-dataset">Save Config</button>
149
+ <button class="btn btn-primary" id="btn-load-preprocess" title="Real discovery/decode/validation with ledger evidence">Load &amp; Preprocess</button>
150
  <button class="btn btn-secondary" id="btn-preprocess-dataset">Load Catalog Metadata</button>
151
  <button class="btn btn-secondary" id="btn-browse-source" title="List and preview images under Images Path">Validate &amp; Browse Images Path</button>
152
+ <button class="btn btn-secondary" id="btn-yaml-validate" title="Semantic YAML path/class validation">Validate YAML</button>
153
  </div>
154
+ <div id="config-exec-status" class="config-exec-status" style="margin-top:10px;font-size:12px;color:var(--text-muted);"></div>
155
  <div id="source-browse-status" class="status-box" style="margin-top:12px;"></div>
156
  <div id="source-browse-gallery" class="output-browse-gallery" style="margin-top:12px;"></div>
157
+ <div class="evidence-card" id="evidence-dataset" data-stage="dataset">
158
+ <h4>What actually happened?</h4>
159
+ <p class="evidence-empty">No execution yet — Load &amp; Preprocess to generate verifiable evidence.</p>
160
+ </div>
161
  </div>
162
 
163
  <div class="card card-glass">
 
169
  </div>
170
  <div class="info-row">
171
  <span class="lbl">Train script:</span>
172
+ <span class="val font-mono" id="script-status-train">Verifying…</span>
173
  </div>
174
  <div class="info-row">
175
  <span class="lbl">Infer script:</span>
176
+ <span class="val font-mono" id="script-status-infer">Verifying…</span>
177
  </div>
178
  <div class="info-row">
179
  <span class="lbl">Auto-Grader script:</span>
180
+ <span class="val font-mono" id="script-status-grader">Verifying…</span>
181
+ </div>
182
+ <div class="info-row">
183
+ <span class="lbl">Backend health:</span>
184
+ <span class="val font-mono" id="health-status-chip">—</span>
185
  </div>
186
  </div>
187
  </div>
 
362
  <p class="text-muted">Generate 2D SAM masks and lift to 3D bounding box proposals.</p>
363
  <div class="form-group">
364
  <label for="perception-sam-checkpoint">SAM Checkpoint</label>
365
+ <input type="text" id="perception-sam-checkpoint" value="models/sam_vit_b.pth" class="input-field"
366
+ placeholder="models/sam_vit_b.pth — download ViT-B weights locally (not bundled on HF Space CPU)">
367
+ <p class="text-muted" style="font-size:12px;margin-top:4px;">
368
+ Default path <code>models/sam_vit_b.pth</code>. On Hugging Face Space, upload weights or run locally with GPU.
369
+ </p>
370
  </div>
371
  <div class="form-group">
372
  <label for="perception-device">Device</label>
 
375
  <option value="cuda">CUDA</option>
376
  </select>
377
  </div>
378
+ <div class="form-group">
379
+ <label><input type="checkbox" id="perception-no-sam"> Skip SAM (fallback / NOT a model SUCCESS)</label>
380
+ </div>
381
  <button class="btn btn-primary" id="btn-perception-run">Run Auto-Label</button>
382
  <div id="perception-status" class="status-box" style="margin-top:16px;"></div>
383
+ <div class="evidence-card" id="evidence-perception" data-stage="perception">
384
+ <h4>What actually happened?</h4>
385
+ <p class="evidence-empty">No auto-label execution yet.</p>
386
+ </div>
387
  </div>
388
  <div class="card card-glass pipeline-output-browser" style="margin-top:16px;" data-browser="perception">
389
  <h3>Pipeline Outputs — proposals &amp; frames</h3>
 
419
  <button class="btn btn-primary" id="btn-tracking-run">Run Temporal Tracker</button>
420
  <div id="tracking-status" class="status-box" style="margin-top:16px;"></div>
421
  <div id="tracking-tracks-table" style="margin-top:16px;"></div>
422
+ <div class="evidence-card evidence-stub" id="evidence-tracking" data-stage="tracking">
423
+ <h4>What actually happened?</h4>
424
+ <p class="evidence-empty">Ledger wiring pending — status will show NOT_EXECUTED until this stage records evidence.</p>
425
+ </div>
426
  </div>
427
  </section>
428
 
 
434
  <button class="btn btn-primary" id="btn-quality-gate-run">Run Quality Gate</button>
435
  <div id="quality-gate-status" class="status-box" style="margin-top:16px;"></div>
436
  <div id="quality-gate-metrics" style="margin-top:16px;"></div>
437
+ <div class="evidence-card evidence-stub" id="evidence-quality-gate" data-stage="quality-gate">
438
+ <h4>What actually happened?</h4>
439
+ <p class="evidence-empty">Evidence panel stub — gate results are live, full ledger wiring deferred.</p>
440
+ </div>
441
  </div>
442
  </section>
443
 
 
557
  <div class="progress-bar-fill" id="train-progress-bar" style="width: 0%"></div>
558
  </div>
559
  </div>
560
+ <div class="evidence-card" id="evidence-training" data-stage="training">
561
+ <h4>What actually happened?</h4>
562
+ <p class="evidence-empty">No training execution yet.</p>
563
+ </div>
564
  </div>
565
 
566
  <div class="card card-glass">
 
601
  <span class="range-val" id="val-infer-iou">0.45</span>
602
  </div>
603
  <button class="btn btn-primary btn-large" id="btn-run-infer">⚡ Run Auto-Labeler</button>
604
+ <div class="evidence-card" id="evidence-inference" data-stage="inference">
605
+ <h4>What actually happened?</h4>
606
+ <p class="evidence-empty">No inference execution yet.</p>
607
+ </div>
608
  </div>
609
 
610
  <div class="card card-glass">
 
646
  </div>
647
  </div>
648
  <button class="btn btn-primary width-full" id="btn-run-grader" style="margin-top: 15px;">Run Quality Diagnostics</button>
649
+ <div class="evidence-card" id="evidence-grader" data-stage="grader">
650
+ <h4>What actually happened?</h4>
651
+ <p class="evidence-empty">No grader execution yet.</p>
652
+ </div>
653
  </div>
654
 
655
  <div class="card card-glass">
 
1118
  </div>
1119
  </div>
1120
 
1121
+ <div id="toast-stack" class="toast-stack" aria-live="polite" aria-relevant="additions"></div>
1122
+
1123
+ <div id="exec-console-modal" class="modal-overlay hidden" role="dialog" aria-label="Execution Console">
1124
+ <div class="modal-card exec-console-card">
1125
+ <div class="modal-header" style="display:flex;justify-content:space-between;align-items:center;gap:12px;">
1126
+ <h3 style="margin:0;">Global Execution Console</h3>
1127
+ <div style="display:flex;gap:8px;">
1128
+ <button type="button" class="btn btn-secondary" id="btn-refresh-executions">Refresh</button>
1129
+ <button type="button" class="btn btn-secondary" id="btn-close-exec-console">Close</button>
1130
+ </div>
1131
+ </div>
1132
+ <p style="font-size:12px;color:var(--text-muted);margin:8px 0 12px;">Persistent ledger under <code>runs/executions/</code>. UI PASS badges are not evidence.</p>
1133
+ <div id="exec-console-list" class="exec-console-list"></div>
1134
+ <div id="exec-console-detail" class="exec-console-detail hidden"></div>
1135
+ </div>
1136
+ </div>
1137
+
1138
  <script src="app.js"></script>
1139
  </body>
1140
  </html>
static/style.css CHANGED
@@ -669,6 +669,117 @@ body {
669
  color: var(--secondary);
670
  }
671
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  /* Animations */
673
  @keyframes fadeIn {
674
  from { opacity: 0; transform: translateY(8px); }
@@ -1442,3 +1553,100 @@ body {
1442
  color: var(--text-muted);
1443
  font-size: 10px;
1444
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
  color: var(--secondary);
670
  }
671
 
672
+ .status-msg.success-msg {
673
+ background: rgba(16, 185, 129, 0.12);
674
+ border-color: rgba(16, 185, 129, 0.28);
675
+ color: var(--success);
676
+ }
677
+
678
+ .status-msg.error-msg {
679
+ background: rgba(239, 68, 68, 0.12);
680
+ border-color: rgba(239, 68, 68, 0.28);
681
+ color: var(--danger);
682
+ }
683
+
684
+ /* Non-blocking toast notifications (replaces window.alert) */
685
+ .toast-stack {
686
+ position: fixed;
687
+ bottom: 24px;
688
+ left: 50%;
689
+ transform: translateX(-50%);
690
+ display: flex;
691
+ flex-direction: column-reverse;
692
+ gap: 8px;
693
+ z-index: 2000;
694
+ width: min(420px, calc(100vw - 32px));
695
+ pointer-events: none;
696
+ }
697
+
698
+ .toast {
699
+ pointer-events: auto;
700
+ display: flex;
701
+ align-items: flex-start;
702
+ gap: 10px;
703
+ padding: 12px 14px;
704
+ border-radius: 8px;
705
+ background: rgba(15, 20, 34, 0.96);
706
+ border: 1px solid var(--border);
707
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
708
+ color: var(--text);
709
+ font-size: 13px;
710
+ line-height: 1.45;
711
+ white-space: pre-line;
712
+ animation: toastIn 0.2s ease;
713
+ }
714
+
715
+ .toast.toast-out {
716
+ animation: toastOut 0.18s ease forwards;
717
+ }
718
+
719
+ .toast-icon {
720
+ flex-shrink: 0;
721
+ width: 18px;
722
+ text-align: center;
723
+ font-size: 14px;
724
+ line-height: 1.3;
725
+ }
726
+
727
+ .toast-body {
728
+ flex: 1;
729
+ min-width: 0;
730
+ }
731
+
732
+ .toast-close {
733
+ flex-shrink: 0;
734
+ background: transparent;
735
+ border: none;
736
+ color: var(--text-muted);
737
+ cursor: pointer;
738
+ font-size: 16px;
739
+ line-height: 1;
740
+ padding: 0 2px;
741
+ }
742
+
743
+ .toast-close:hover {
744
+ color: var(--text);
745
+ }
746
+
747
+ .toast-success {
748
+ border-color: rgba(16, 185, 129, 0.35);
749
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(16, 185, 129, 0.08);
750
+ }
751
+
752
+ .toast-success .toast-icon { color: var(--success); }
753
+
754
+ .toast-error {
755
+ border-color: rgba(239, 68, 68, 0.4);
756
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(239, 68, 68, 0.08);
757
+ }
758
+
759
+ .toast-error .toast-icon { color: var(--danger); }
760
+
761
+ .toast-warning {
762
+ border-color: rgba(245, 158, 11, 0.4);
763
+ }
764
+
765
+ .toast-warning .toast-icon { color: var(--warning); }
766
+
767
+ .toast-info {
768
+ border-color: rgba(59, 130, 246, 0.35);
769
+ }
770
+
771
+ .toast-info .toast-icon { color: var(--secondary); }
772
+
773
+ @keyframes toastIn {
774
+ from { opacity: 0; transform: translateY(10px); }
775
+ to { opacity: 1; transform: translateY(0); }
776
+ }
777
+
778
+ @keyframes toastOut {
779
+ from { opacity: 1; transform: translateY(0); }
780
+ to { opacity: 0; transform: translateY(8px); }
781
+ }
782
+
783
  /* Animations */
784
  @keyframes fadeIn {
785
  from { opacity: 0; transform: translateY(8px); }
 
1553
  color: var(--text-muted);
1554
  font-size: 10px;
1555
  }
1556
+
1557
+ /* Execution integrity */
1558
+ .strict-mode-toggle {
1559
+ display: inline-flex;
1560
+ align-items: center;
1561
+ gap: 6px;
1562
+ font-size: 12px;
1563
+ color: var(--text-muted);
1564
+ cursor: pointer;
1565
+ user-select: none;
1566
+ }
1567
+ .evidence-card {
1568
+ margin-top: 16px;
1569
+ padding: 14px;
1570
+ border: 1px solid var(--border);
1571
+ border-radius: 8px;
1572
+ background: rgba(4, 6, 11, 0.55);
1573
+ }
1574
+ .evidence-card h4 {
1575
+ margin: 0 0 10px 0;
1576
+ font-size: 13px;
1577
+ color: var(--secondary);
1578
+ }
1579
+ .evidence-empty {
1580
+ font-size: 12px;
1581
+ color: var(--text-muted);
1582
+ margin: 0;
1583
+ }
1584
+ .ev-status {
1585
+ display: inline-block;
1586
+ font-size: 11px;
1587
+ font-weight: 700;
1588
+ letter-spacing: 0.4px;
1589
+ padding: 3px 8px;
1590
+ border-radius: 4px;
1591
+ margin-bottom: 10px;
1592
+ font-family: var(--font-mono);
1593
+ }
1594
+ .ev-status-SUCCEEDED { background: #00ffaa22; color: #00ffaa; }
1595
+ .ev-status-PARTIAL_SUCCESS { background: #fbbf2422; color: #fbbf24; }
1596
+ .ev-status-FAILED, .ev-status-VALIDATION_FAILED { background: #ff444422; color: #ff8888; }
1597
+ .ev-status-RUNNING, .ev-status-QUEUED { background: #3b82f622; color: #93c5fd; }
1598
+ .ev-status-NOT_EXECUTED, .ev-status-UNVERIFIED, .ev-status-CANCELLED { background: #64748b33; color: #94a3b8; }
1599
+ .ev-row {
1600
+ display: flex;
1601
+ justify-content: space-between;
1602
+ gap: 12px;
1603
+ font-size: 12px;
1604
+ padding: 3px 0;
1605
+ border-bottom: 1px solid rgba(255,255,255,0.04);
1606
+ }
1607
+ .ev-row span { color: var(--text-muted); }
1608
+ .ev-row strong { color: var(--text); text-align: right; word-break: break-all; }
1609
+ .ev-msg { font-size: 12px; color: var(--text-muted); margin: 10px 0 0; }
1610
+ .ev-next { margin-top: 12px; font-size: 12px; color: var(--text-muted); }
1611
+ .ev-next ul { margin: 6px 0 0 18px; padding: 0; }
1612
+ .ev-next li { margin-bottom: 4px; }
1613
+ .ev-log-link { display: inline-block; margin-top: 10px; font-size: 12px; color: #38bdf8; }
1614
+ .ev-timeline { margin-top: 10px; font-size: 11px; color: var(--text-muted); }
1615
+ .ev-timeline ul { margin: 6px 0 0 16px; padding: 0; }
1616
+ .exec-console-card {
1617
+ width: min(920px, 94vw);
1618
+ max-height: 85vh;
1619
+ overflow: auto;
1620
+ background: var(--bg-sidebar);
1621
+ border: 1px solid var(--border);
1622
+ border-radius: 12px;
1623
+ padding: 18px;
1624
+ }
1625
+ .exec-console-list { display: flex; flex-direction: column; gap: 6px; }
1626
+ .exec-row {
1627
+ display: grid;
1628
+ grid-template-columns: 140px 160px 1fr 80px 90px;
1629
+ gap: 8px;
1630
+ align-items: center;
1631
+ text-align: left;
1632
+ padding: 8px 10px;
1633
+ background: rgba(4,6,11,0.5);
1634
+ border: 1px solid var(--border);
1635
+ border-radius: 6px;
1636
+ color: var(--text);
1637
+ cursor: pointer;
1638
+ font-size: 12px;
1639
+ }
1640
+ .exec-row:hover { border-color: var(--border-active); }
1641
+ .exec-console-detail pre {
1642
+ margin-top: 12px;
1643
+ max-height: 320px;
1644
+ overflow: auto;
1645
+ background: #04060b;
1646
+ border: 1px solid var(--border);
1647
+ border-radius: 8px;
1648
+ padding: 12px;
1649
+ font-size: 11px;
1650
+ color: #00ffaa;
1651
+ }
1652
+ .config-exec-status { font-family: var(--font-mono); }
tests/test_pipeline/test_auto_label_integrity.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Auto-label pre-checks, status semantics, and UI-facing error payloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+ from fastapi.testclient import TestClient
11
+
12
+ ROOT = Path(__file__).resolve().parents[2]
13
+ if str(ROOT) not in sys.path:
14
+ sys.path.insert(0, str(ROOT))
15
+
16
+
17
+ def _write_tiny_jpeg(path: Path) -> None:
18
+ path.write_bytes(
19
+ bytes.fromhex(
20
+ "ffd8ffe000104a46494600010100000100010000ffdb004300080606070605080707"
21
+ "070909080a0c140d0c0b0b0c1912130f141d1a1f1e1d1a1c1c20242e2720222c231c"
22
+ "1c2837292c30313434341f27393d38323c2e333432ffdb0043010909090c0b0c180d"
23
+ "0d1832211c2132323232323232323232323232323232323232323232323232323232"
24
+ "323232323232323232323232323232323232323232ffc00011080001000103011100"
25
+ "021101031101ffc40014000100000000000000000000000000000000ffc400141001"
26
+ "00000000000000000000000000000000ffda000c0301000210031000003f00bf80ffd9"
27
+ )
28
+ )
29
+
30
+
31
+ @pytest.fixture()
32
+ def client(tmp_path, monkeypatch):
33
+ monkeypatch.chdir(tmp_path)
34
+ from sensorflow import execution_ledger as ledger
35
+
36
+ monkeypatch.setattr(ledger, "LEDGER_ROOT", tmp_path / "runs" / "executions")
37
+ monkeypatch.setattr(ledger, "INDEX_PATH", tmp_path / "runs" / "executions" / "index.json")
38
+ monkeypatch.setattr(ledger, "STRICT_MODE_PATH", tmp_path / "runs" / "studio_strict_mode.json")
39
+
40
+ import importlib
41
+ import app_backend
42
+
43
+ importlib.reload(app_backend)
44
+ return TestClient(app_backend.app)
45
+
46
+
47
+ def _ingest_local(client: TestClient, tmp_path: Path, seq_id: str = "seq_local", n: int = 5):
48
+ frames_dir = tmp_path / "frames"
49
+ frames_dir.mkdir()
50
+ for i in range(n):
51
+ _write_tiny_jpeg(frames_dir / f"f{i}.jpg")
52
+ res = client.post(
53
+ "/api/dataset/ingest",
54
+ json={
55
+ "vendors": ["local"],
56
+ "sequence_id": seq_id,
57
+ "source_path": str(frames_dir),
58
+ "max_frames": 10000,
59
+ },
60
+ )
61
+ assert res.status_code == 200
62
+ assert res.json()["frames"] == n
63
+ return res
64
+
65
+
66
+ def test_missing_checkpoint_returns_failed_not_partial(client, tmp_path):
67
+ _ingest_local(client, tmp_path, "seq_ckpt", 3)
68
+ res = client.post(
69
+ "/api/perception/auto-label",
70
+ json={
71
+ "sequence_id": "seq_ckpt",
72
+ "sam_checkpoint": str(tmp_path / "missing_sam.pth"),
73
+ "no_sam": False,
74
+ },
75
+ )
76
+ assert res.status_code == 400
77
+ detail = res.json()["detail"]
78
+ assert detail["status"] == "FAILED"
79
+ assert detail["checkpoint"]["exists"] is False
80
+ assert detail["frames_expected"] == 3
81
+ assert detail["frames_processed"] == 0
82
+ assert detail.get("next_steps")
83
+ assert "FAILED" == detail["status"]
84
+ assert "PARTIAL_SUCCESS" != detail["status"]
85
+
86
+
87
+ def test_empty_manifest_returns_failed(client, tmp_path):
88
+ manifest_dir = tmp_path / "runs" / "pipeline" / "seq_empty"
89
+ manifest_dir.mkdir(parents=True)
90
+ (manifest_dir / "manifest.json").write_text(
91
+ json.dumps(
92
+ {
93
+ "sequence_id": "seq_empty",
94
+ "vendor": "local",
95
+ "frames": [],
96
+ "taxonomy_manifest": {"demo_stub": False},
97
+ }
98
+ )
99
+ )
100
+ res = client.post(
101
+ "/api/perception/auto-label",
102
+ json={"sequence_id": "seq_empty", "no_sam": True},
103
+ )
104
+ assert res.status_code == 400
105
+ detail = res.json()["detail"]
106
+ assert detail["status"] == "FAILED"
107
+ assert detail["frames_expected"] == 0
108
+ assert "ingest" in detail["message"].lower() or "0 ingested" in detail["message"].lower()
109
+
110
+
111
+ def test_ingest_then_no_sam_auto_label_has_frames(client, tmp_path):
112
+ _ingest_local(client, tmp_path, "seq_ok", 4)
113
+ res = client.post(
114
+ "/api/perception/auto-label",
115
+ json={"sequence_id": "seq_ok", "no_sam": True},
116
+ )
117
+ assert res.status_code == 200
118
+ body = res.json()
119
+ assert body["frames_expected"] == 4
120
+ assert body["frames_processed"] == 4
121
+ assert body["status"] == "NOT_EXECUTED"
122
+ assert body["sam_ran"] is False
123
+ assert isinstance(body.get("message"), str)
124
+ assert "{" not in body["message"][:20]