fffiloni commited on
Commit
97ceb69
·
verified ·
1 Parent(s): f284757

Upload 8 files

Browse files
web/static/app.css CHANGED
@@ -2793,7 +2793,7 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
2793
 
2794
  /* v190.10 — Active Run timeline hotfix */
2795
 
2796
- /* v190.14 — Build-first Runs Explorer polish */
2797
  .runs-table{
2798
  gap:12px!important;
2799
  }
@@ -2857,3 +2857,59 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
2857
  .run-row.run-list-item{padding:13px 10px!important;border-radius:18px!important}
2858
  .linked-validation-strip{width:100%;justify-content:space-between;border-radius:14px}
2859
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2793
 
2794
  /* v190.10 — Active Run timeline hotfix */
2795
 
2796
+ /* v190.15 — Build-first Runs Explorer polish */
2797
  .runs-table{
2798
  gap:12px!important;
2799
  }
 
2857
  .run-row.run-list-item{padding:13px 10px!important;border-radius:18px!important}
2858
  .linked-validation-strip{width:100%;justify-content:space-between;border-radius:14px}
2859
  }
2860
+
2861
+ /* v190.15 — Build-first compatibility cleanup */
2862
+ .run-row.run-list-item{border-left:0!important;border-right:0!important}
2863
+ .run-row.build-run{background:linear-gradient(180deg,#fff,#fbfaf7)}
2864
+ .run-row .linked-validation-strip{margin-top:10px}
2865
+
2866
+ /* v190.16 — Delete cascade clarity */
2867
+ .delete-run-meta span strong{letter-spacing:.01em}
2868
+ .delete-run-meta span code{word-break:break-word}
2869
+ .delete-run-card{
2870
+ border-left:0!important;
2871
+ border-right:0!important;
2872
+ box-shadow:0 24px 80px rgba(15,23,42,.18)!important;
2873
+ }
2874
+ .delete-run-warning{
2875
+ border-left:0!important;
2876
+ border-right:0!important;
2877
+ }
2878
+
2879
+ /* v190.16 — Build-first Runs Explorer polish */
2880
+ /* v190.16 — Build-first compatibility cleanup */
2881
+
2882
+ /* v190.17 — Effective eval and linked validation provenance */
2883
+ .eval-effective-note{
2884
+ margin:.45rem 0 .7rem;
2885
+ padding:.55rem .7rem;
2886
+ border:0;
2887
+ border-radius:14px;
2888
+ background:rgba(34,197,94,.08);
2889
+ color:#31513a;
2890
+ }
2891
+ .eval-effective-note code{font-size:.78rem;}
2892
+ .metrics-list li em{
2893
+ display:block;
2894
+ margin-top:.18rem;
2895
+ color:var(--muted,#6b7280);
2896
+ font-size:.75rem;
2897
+ font-style:normal;
2898
+ }
2899
+
2900
+ /* v190.17 — Delete cascade clarity */
2901
+
2902
+ /* v190.17 — Build-first Runs Explorer polish */
2903
+
2904
+ /* v190.17 — Build-first compatibility cleanup */
2905
+
2906
+ /* v190.19 — Build completion matrix + endpoint discovery-first linked Space Test */
2907
+ .space-test-policy-chip{display:inline-flex;align-items:center;gap:.4rem;border-radius:999px;padding:.35rem .65rem;background:rgba(99,102,241,.09);color:#3730a3;font-weight:650;border:0;box-shadow:none}
2908
+ .space-test-policy-chip.recover{background:rgba(245,158,11,.12);color:#92400e}
2909
+ .space-test-policy-chip.unavailable,.space-test-policy-chip.blocked{background:rgba(100,116,139,.12);color:#475569}
2910
+ .endpoint-discovery-note{margin-top:.55rem;padding:.7rem .85rem;border-radius:14px;background:rgba(14,165,233,.08);color:#075985;border:0;line-height:1.45}
2911
+ .recovered-build-chip{display:inline-flex;gap:.35rem;align-items:center;border-radius:999px;padding:.25rem .55rem;background:rgba(16,185,129,.12);color:#047857;border:0}
2912
+ /* v190.19 — Build-first Runs Explorer polish */
2913
+ /* v190.19 — Build-first compatibility cleanup */
2914
+ /* v190.19 — Delete cascade clarity */
2915
+ /* v190.19 — Effective eval and linked validation provenance */
web/static/app.js CHANGED
@@ -344,11 +344,13 @@ function renderAnonymousEvalStatus(me){
344
  const enable=$('enableEvalArchive');
345
  const disable=$('disableEvalArchive');
346
  const verify=$('verifyEvalArchive');
 
347
  const stateBadge=$('evalArchiveStateBadge');
348
  const cfg=me?.anonymous_eval||{};
349
  const enabled=Boolean(cfg.enabled);
350
  const canManage=Boolean(cfg.can_manage)&&cfg.env_override!==true;
351
  const readonly=Boolean(cfg.readonly)||Boolean(enabled&&!canManage);
 
352
  const bucket=cfg.bucket_source||cfg.proposed_bucket_source||'configured bucket';
353
  const path=cfg.bucket_path?'/'+cfg.bucket_path:(cfg.default_bucket_path?'/'+cfg.default_bucket_path:'');
354
  const mount=cfg.job_mount_path||cfg.default_mount_path||'/evals';
@@ -396,6 +398,11 @@ function renderAnonymousEvalStatus(me){
396
  verify.disabled=state.busy;
397
  verify.title='Refresh and verify the eval archive configuration.';
398
  }
 
 
 
 
 
399
  }
400
  function setBucketReady(ok,message){state.bucketReady=Boolean(ok);setBuildLaunchEnabled();const feedback=state.bucketReady?'Ready for builds, validations and artifacts.':(message||'Create your private run bucket before launching builds.');setText('buildGateText',state.bucketReady?'Bucket ready. You can launch builds.':bucketFeedbackSummary(feedback));setText('bucketInlineStatus',state.bucketReady?'Ready':'Setup needed');setBucketFeedback(state.bucketReady?'ready':'error',feedback);for(const id of ['bucketInlineDot','sidebarBucketDot']){const dot=$(id);if(dot)dot.className='status-dot '+(state.bucketReady?'green':'amber-dot')}const inline=$('bucketInlineStatus');if(inline)inline.className='badge '+(state.bucketReady?'success':'warn');document.querySelectorAll('[data-requires-bucket="true"]').forEach(btn=>btn.disabled=state.busy||!state.bucketReady);setBuildLaunchEnabled()}
401
  function effectivePreferredHardware(){const preferred=formValue('preferredHardware')||'zero-a10g';const fallback=formValue('fallbackHardware')||'a10g-large';const tryZero=$('tryZeroGpu')?.checked??true;if(tryZero)return preferred;if(preferred==='zero-a10g'||preferred.startsWith('zero'))return fallback;return preferred}
@@ -643,6 +650,20 @@ async function disableEvalArchive(){
643
  }
644
 
645
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646
  async function checkBucket(noisy=true){setButtonBusy('checkBucket',true,'Checking…');setBucketFeedback('checking','Checking private bucket access…');try{const data=await apiGet(`/api/bucket/status?bucket_name=${encodeURIComponent(state.bucketName)}`);const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Create your private run bucket before launching builds.');if(noisy)showMessage(data.ok?`Bucket ready: ${source}`:`Bucket not ready: ${data.error||'not found'}`,!data.ok);return data}catch(e){setBucketReady(false,e.message);if(noisy)showMessage(e.message,true);throw e}finally{setButtonBusy('checkBucket',false)}}
647
  async function createBucket(){setButtonBusy('createBucket',true,'Creating…');setBucketFeedback('creating','Creating private bucket…');try{const data=await apiPost('/api/bucket/create',{bucket_name:state.bucketName});const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Could not create bucket.');showMessage(data.ok?`Created/ready: ${source}`:`Could not create bucket: ${data.error}`,!data.ok);if(data.ok)loadRuns(true)}catch(e){setBucketReady(false,e.message);showMessage(e.message,true)}finally{setButtonBusy('createBucket',false)}}
648
 
@@ -746,11 +767,12 @@ function validationPrefillFromDetail(detail={}){
746
  let kwargs=firstObjectPath(smoke.test_kwargs,smoke.kwargs,gate.test_kwargs,gateSmoke.test_kwargs,launch.test_kwargs_json,summary.test_kwargs_json);
747
  if(!Array.isArray(args))args=defaultValidationArgs(expected);
748
  if(!kwargs||Array.isArray(kwargs)||typeof kwargs!=='object')kwargs={};
749
- const target=summary.target_space||stateObj.target_space||launch.target_space||progress.target_space||$('detailSpace')?.textContent||'';
750
- const api=normalizeApiName(smoke.api_name||gate.api_name||launch.api_name||gateSmoke.api_name||progress.api_name||'/generate');
751
- const parentBuildRunId=detail.run_id||detail.summary?.run_id||state.runId||progress.run_id||'';
752
- const automaticSmokePassed=Boolean(smoke.ok||smoke.status==='success');
753
- return {target,api,expected,args,kwargs,parentBuildRunId,sourceRunId:parentBuildRunId,linkedTargetSpace:target,automaticSmokePassed,smokeLatency:smoke.latency_seconds||smoke.observed_latency_seconds};
 
754
  }
755
  function renderValidationStatus(data={}){
756
  const candidateRunId=validationDetailRunId(data)||state.validationRunId;
@@ -879,10 +901,10 @@ function renderValidationPrefillDraft(prefill={},sourceLabel='selected Space'){
879
  if(tabDot)tabDot.className='tab-status-dot neutral';
880
  const card=$('spaceTestPreview');
881
  if(card)card.classList.remove('is-validating');
882
- setText('spaceTestOutput',prefill.automaticSmokePassed?'Replay linked smoke test':'New linked validation');
883
  setText('validationStartedAt','Not launched');
884
  setText('spaceTestLatency','—');
885
- setText('spaceTestVerdict',prefill.automaticSmokePassed?`Automatic smoke passed · ready to replay from ${sourceLabel}`:`Linked to ${sourceLabel}`);
886
  setText('spaceTestProgressPercent','0%');
887
  const fill=$('spaceTestProgressFill');
888
  if(fill){fill.style.width='0%';fill.className='validation-progress-fill neutral'}
@@ -893,7 +915,7 @@ function renderValidationPrefillDraft(prefill={},sourceLabel='selected Space'){
893
  const endpoints=$('spaceTestEndpoints');
894
  if(endpoints)endpoints.innerHTML=`<div class="empty compact-empty">Linked to Build Run <code>${escapeHtml(state.validationPrefillDraft.parentBuildRunId||'—')}</code>. Endpoint schema will be refreshed when this validation runs.</div>`;
895
  const launchBtn=$('launchValidate');
896
- if(launchBtn&&!launchBtn.classList.contains('is-busy'))launchBtn.textContent='Launch validation';
897
  setValidationLaunchEnabled();
898
  const deleteBtn=$('deleteValidationRun');
899
  if(deleteBtn){deleteBtn.disabled=true;deleteBtn.onclick=null;}
@@ -917,11 +939,30 @@ function prepareValidationFromActiveRun(){
917
  const activeDetail={...selected,...(state.activeRunProgress||{}),summary:{...(selected.summary||{}),...(state.activeRunProgress?.summary||{})}};
918
  applyValidationPrefill(activeDetail,'active run',{preserveSelection:false});
919
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
920
  function prepareValidationFromSelectedRun(){
921
  applyValidationPrefill(state.selectedRunDetail||{},'selected run',{preserveSelection:false});
922
  }
923
  async function launchBuild(){const formError=validateBuildForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching build Job…');setButtonBusy('launchBuild',true,'Launching…');try{setText('hardwarePlan',hardwarePlanLabel());const payload={bucket_name:state.bucketName,model_id:formValue('modelId'),target_space_name:formValue('targetSpace')||undefined,pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.checked??true,try_zero_gpu_first:$('tryZeroGpu')?.checked??true,expected_output_type:formValue('buildExpectedOutput')};const result=await apiPost('/api/build',payload);setActiveRun(result);showMessage(`Build launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchBuild',false)}}
924
- async function launchValidate(){const formError=validateValidationForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching validation Job…');setButtonBusy('launchValidate',true,'Launching…');try{const args=parseJsonField('testArgs',[]);const kwargs=parseJsonField('testKwargs',{});const payload={bucket_name:state.bucketName,parent_build_run_id:(state.validationPrefillDraft||{}).parentBuildRunId||state.linkedValidationParent,target_space_id:formValue('validateSpace'),api_name:normalizeApiName(formValue('validateApi')||'/generate'),expected_output_type:formValue('expectedOutput')||'any',test_args_json:prettyJson(args),test_kwargs_json:prettyJson(kwargs),live_timeout_seconds:validationTimeoutValue()};const result=await apiPost('/api/validate',payload);state.validationPrefillDraft=null;state.validationArgsDirty=false;state.validationMode='running';renderValidationLaunchResult(result);startValidationPolling(result.run_id);showMessage(`Validation launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchValidate',false)}}
925
  async function refreshRunReportPreview(runId){
926
  if(!runId)return;
927
  try{
@@ -967,11 +1008,12 @@ function readRunLatencyPayload(p={}){
967
  const latency=firstPositiveNumber(smoke.latency_seconds,smoke.observed_latency_seconds,summary.latency_seconds,summary.observed_latency_seconds,p.latency_seconds,p.observed_latency_seconds,manual.latency_seconds,manual.observed_latency_seconds,eventLatency);
968
  const duration=firstPositiveNumber(smoke.recommended_zerogpu_duration_seconds,smoke.recommended_zero_gpu_duration_seconds,smoke.recommended_duration_seconds,summary.recommended_zerogpu_duration_seconds,summary.recommended_zero_gpu_duration_seconds,summary.recommended_duration_seconds,manual.recommended_zero_gpu_duration_seconds,gate.recommended_zerogpu_duration_seconds,gate.recommended_zero_gpu_duration_seconds,gate.zero_gpu_duration_recommendation,eventDuration);
969
  const source=smoke.recommendation_source||summary.recommendation_source||manual.recommendation_source||gate.recommendation_source||(manual.status==='success'?'linked Space Test':'')||(eventLatency?'generation_smoke event':'');
 
970
  const failureType=smoke.failure_type||validation.failure_type||gate.failure_type||'';
971
  const failureOwner=smoke.failure_owner||validation.failure_owner||gate.failure_owner||'';
972
  const missingArgument=smoke.missing_argument||validation.missing_argument||gate.missing_argument||'';
973
  const error=smoke.error||smoke.message||validation.failure_reason||gate.message||'';
974
- return {smoke,gate,latency,duration,source,failureType,failureOwner,missingArgument,error};
975
  }
976
  function smokeFailureLatencyNote({smoke={},failureType='',failureOwner='',missingArgument='',error=''}={}){
977
  const status=String(smoke.status||'').toLowerCase();
@@ -996,7 +1038,7 @@ function renderActiveLatency(p={}){
996
  const runId=String(p.run_id||p.summary?.run_id||state.runId||'');
997
  state.latencyCache=state.latencyCache||{};
998
  const payload=readRunLatencyPayload(p);
999
- const {latency,duration,source}=payload;
1000
  const hasLatency=Number.isFinite(latency)&&latency>0;
1001
  const hasDuration=Number.isFinite(duration)&&duration>0;
1002
  const cached=state.latencyCache[runId]||{};
@@ -1006,7 +1048,8 @@ function renderActiveLatency(p={}){
1006
  const finalBlocker=p.build_error_observation?' Final blocker: build error after repair.':'';
1007
  const failedWithoutSmoke=(runStatus==='failed'||runStatus==='error')&&!hasLatency;
1008
  const failureNote=smokeFailureLatencyNote(payload)||(failedWithoutSmoke?`Not measured because live validation did not reach a successful generation smoke test.${finalBlocker}`:'');
1009
- const stableSource=hasLatency?(source||'live smoke test'):(failureNote?'not measured: smoke failed':(cached.source||'measured when smoke test passes'));
 
1010
  if(hasLatency||hasDuration)state.latencyCache[runId]={latency:stableLatency,duration:stableDuration,source:stableSource};
1011
  const finalHasLatency=Number.isFinite(stableLatency)&&stableLatency>0;
1012
  const finalHasDuration=Number.isFinite(stableDuration)&&stableDuration>0;
@@ -1016,7 +1059,7 @@ function renderActiveLatency(p={}){
1016
  const note=$('activeLatencyNote');
1017
  if(note){
1018
  note.textContent=finalHasDuration
1019
- ? 'This measured latency is normally used to tune the `@spaces.GPU(duration=...)` decorator for ZeroGPU when the generated Space uses ZeroGPU.'
1020
  : (failureNote||'When available, the live smoke-test latency is used to recommend the `@spaces.GPU(duration=...)` value for ZeroGPU apps.');
1021
  }
1022
  card.classList.toggle('has-latency',finalHasLatency);
@@ -1077,7 +1120,7 @@ function markPendingActiveFields(){
1077
  el.classList.toggle('is-pending',pending);
1078
  });
1079
  }
1080
- function renderMetrics(p){const root=$('metricsList');const {smoke,gate,latency,duration}=readRunLatencyPayload(p);const health=(gate.implementation_signals&&gate.implementation_signals.health_passed)||smoke.health_passed;const smokeOk=smoke.ok||smoke.status==='success';renderActiveLatency(p);if(!root)return;root.innerHTML=`<li><span>/health</span><strong class="${health?'metric-ok':''}">${health?'passed':'pending'}</strong></li><li><span>/generate smoke test</span><strong class="${smokeOk?'metric-ok':'metric-warn'}">${smokeOk?'passed':(smoke.status||'pending')}</strong></li><li><span>Latency</span><strong>${Number.isFinite(latency)&&latency>0?formatLatencySeconds(latency):'—'}</strong></li><li><span>Recommended ZeroGPU duration</span><strong>${Number.isFinite(duration)&&duration>0?fmtSeconds(Math.round(duration)):'—'}</strong></li>`}
1081
  function renderEvents(events){const root=$('eventsPanel');if(!root)return;try{const rows=Array.isArray(events)?events:[];root.textContent=rows.slice(-12).map(e=>`${e?.ts||''} ${e?.step||''} ${e?.status||''} ${e?.message||''}`).join('\n')||'No events yet. The run may still be scheduling.'}catch(e){console.warn('renderEvents failed',e);root.textContent='Events could not be rendered.'}}
1082
  async function cancelActiveRun(){
1083
  if(!state.runId){showMessage('Select a running run before cancelling.', 'warning');return}
@@ -1135,6 +1178,7 @@ function bindActions(){
1135
  enableEvalArchive:enableEvalArchive,
1136
  verifyEvalArchive:verifyEvalArchive,
1137
  disableEvalArchive:disableEvalArchive,
 
1138
  refreshRuns:()=>loadRuns(true),
1139
  refreshRunsHome:()=>loadRuns(true),
1140
  cancelRun:cancelActiveRun,
@@ -1158,3 +1202,7 @@ bindNavigation();bindActions();bindRunFilters();bindValidationDraftInputs();setT
1158
  // 'Eval archive off · set env vars'
1159
  // Anonymous project eval archive enabled:
1160
  // Env override remains supported for backend publishing: ASF_EVAL_ENABLED=true, ASF_EVAL_BUCKET_SOURCE=<namespace>/<bucket>, ASF_EVAL_BUCKET_PATH=evals, ASF_EVAL_BUCKET_MOUNT=/evals.
 
 
 
 
 
344
  const enable=$('enableEvalArchive');
345
  const disable=$('disableEvalArchive');
346
  const verify=$('verifyEvalArchive');
347
+ const flush=$('flushEvalArchive');
348
  const stateBadge=$('evalArchiveStateBadge');
349
  const cfg=me?.anonymous_eval||{};
350
  const enabled=Boolean(cfg.enabled);
351
  const canManage=Boolean(cfg.can_manage)&&cfg.env_override!==true;
352
  const readonly=Boolean(cfg.readonly)||Boolean(enabled&&!canManage);
353
+ const canFlush=Boolean(cfg.can_flush)&&enabled;
354
  const bucket=cfg.bucket_source||cfg.proposed_bucket_source||'configured bucket';
355
  const path=cfg.bucket_path?'/'+cfg.bucket_path:(cfg.default_bucket_path?'/'+cfg.default_bucket_path:'');
356
  const mount=cfg.job_mount_path||cfg.default_mount_path||'/evals';
 
398
  verify.disabled=state.busy;
399
  verify.title='Refresh and verify the eval archive configuration.';
400
  }
401
+ if(flush){
402
+ flush.hidden=!canFlush;
403
+ flush.disabled=state.busy;
404
+ flush.title='Delete all anonymized eval records from the mounted operator archive. Config is preserved.';
405
+ }
406
  }
407
  function setBucketReady(ok,message){state.bucketReady=Boolean(ok);setBuildLaunchEnabled();const feedback=state.bucketReady?'Ready for builds, validations and artifacts.':(message||'Create your private run bucket before launching builds.');setText('buildGateText',state.bucketReady?'Bucket ready. You can launch builds.':bucketFeedbackSummary(feedback));setText('bucketInlineStatus',state.bucketReady?'Ready':'Setup needed');setBucketFeedback(state.bucketReady?'ready':'error',feedback);for(const id of ['bucketInlineDot','sidebarBucketDot']){const dot=$(id);if(dot)dot.className='status-dot '+(state.bucketReady?'green':'amber-dot')}const inline=$('bucketInlineStatus');if(inline)inline.className='badge '+(state.bucketReady?'success':'warn');document.querySelectorAll('[data-requires-bucket="true"]').forEach(btn=>btn.disabled=state.busy||!state.bucketReady);setBuildLaunchEnabled()}
408
  function effectivePreferredHardware(){const preferred=formValue('preferredHardware')||'zero-a10g';const fallback=formValue('fallbackHardware')||'a10g-large';const tryZero=$('tryZeroGpu')?.checked??true;if(tryZero)return preferred;if(preferred==='zero-a10g'||preferred.startsWith('zero'))return fallback;return preferred}
 
650
  }
651
 
652
 
653
+ async function flushEvalArchive(){
654
+ const ok=window.confirm('Delete all anonymized eval archive records from the mounted operator bucket? This preserves the eval archive configuration but cannot be undone.');
655
+ if(!ok)return;
656
+ setButtonBusy('flushEvalArchive',true,'Flushing…');
657
+ try{
658
+ const result=await apiPost('/api/eval-archive/flush',{});
659
+ state.user={...(state.user||{}),anonymous_eval:result};
660
+ renderAnonymousEvalStatus(state.user);
661
+ showMessage(result.message||'Eval archive records flushed.',result.partial_flush?'warning':'success');
662
+ }catch(e){showMessage(e.message,true)}
663
+ finally{setButtonBusy('flushEvalArchive',false)}
664
+ }
665
+
666
+
667
  async function checkBucket(noisy=true){setButtonBusy('checkBucket',true,'Checking…');setBucketFeedback('checking','Checking private bucket access…');try{const data=await apiGet(`/api/bucket/status?bucket_name=${encodeURIComponent(state.bucketName)}`);const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Create your private run bucket before launching builds.');if(noisy)showMessage(data.ok?`Bucket ready: ${source}`:`Bucket not ready: ${data.error||'not found'}`,!data.ok);return data}catch(e){setBucketReady(false,e.message);if(noisy)showMessage(e.message,true);throw e}finally{setButtonBusy('checkBucket',false)}}
668
  async function createBucket(){setButtonBusy('createBucket',true,'Creating…');setBucketFeedback('creating','Creating private bucket…');try{const data=await apiPost('/api/bucket/create',{bucket_name:state.bucketName});const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Could not create bucket.');showMessage(data.ok?`Created/ready: ${source}`:`Could not create bucket: ${data.error}`,!data.ok);if(data.ok)loadRuns(true)}catch(e){setBucketReady(false,e.message);showMessage(e.message,true)}finally{setButtonBusy('createBucket',false)}}
669
 
 
767
  let kwargs=firstObjectPath(smoke.test_kwargs,smoke.kwargs,gate.test_kwargs,gateSmoke.test_kwargs,launch.test_kwargs_json,summary.test_kwargs_json);
768
  if(!Array.isArray(args))args=defaultValidationArgs(expected);
769
  if(!kwargs||Array.isArray(kwargs)||typeof kwargs!=='object')kwargs={};
770
+ const policy=detail.space_test_policy||detail.view?.space_test_policy||detail.space_test?.policy||progress.view?.space_test_policy||progress.view?.space_test?.policy||{};
771
+ const target=policy.target_space||summary.target_space||stateObj.target_space||launch.target_space||progress.target_space||$('detailSpace')?.textContent||'';
772
+ const api=policy.requires_endpoint_discovery?'':normalizeApiName(policy.endpoint||smoke.api_name||gate.api_name||launch.api_name||gateSmoke.api_name||progress.api_name||'/generate');
773
+ const parentBuildRunId=policy.parent_build_run_id||detail.run_id||detail.summary?.run_id||state.runId||progress.run_id||'';
774
+ const automaticSmokePassed=Boolean(smoke.ok||smoke.status==='success'||policy.mode==='replay');
775
+ return {target,api,expected,args,kwargs,parentBuildRunId,sourceRunId:parentBuildRunId,linkedTargetSpace:target,automaticSmokePassed,smokeLatency:smoke.latency_seconds||smoke.observed_latency_seconds,spaceTestPolicy:policy,requiresEndpointDiscovery:Boolean(policy.requires_endpoint_discovery)};
776
  }
777
  function renderValidationStatus(data={}){
778
  const candidateRunId=validationDetailRunId(data)||state.validationRunId;
 
901
  if(tabDot)tabDot.className='tab-status-dot neutral';
902
  const card=$('spaceTestPreview');
903
  if(card)card.classList.remove('is-validating');
904
+ setText('spaceTestOutput',prefill.requiresEndpointDiscovery?'Discover endpoints and validate':(prefill.automaticSmokePassed?'Replay linked smoke test':'New linked validation'));
905
  setText('validationStartedAt','Not launched');
906
  setText('spaceTestLatency','—');
907
+ setText('spaceTestVerdict',prefill.spaceTestPolicy?.message|| (prefill.automaticSmokePassed?`Automatic smoke passed · ready to replay from ${sourceLabel}`:`Linked to ${sourceLabel}`));
908
  setText('spaceTestProgressPercent','0%');
909
  const fill=$('spaceTestProgressFill');
910
  if(fill){fill.style.width='0%';fill.className='validation-progress-fill neutral'}
 
915
  const endpoints=$('spaceTestEndpoints');
916
  if(endpoints)endpoints.innerHTML=`<div class="empty compact-empty">Linked to Build Run <code>${escapeHtml(state.validationPrefillDraft.parentBuildRunId||'—')}</code>. Endpoint schema will be refreshed when this validation runs.</div>`;
917
  const launchBtn=$('launchValidate');
918
+ if(launchBtn&&!launchBtn.classList.contains('is-busy'))launchBtn.textContent=prefill.requiresEndpointDiscovery?'Discover endpoints and validate':'Launch validation';
919
  setValidationLaunchEnabled();
920
  const deleteBtn=$('deleteValidationRun');
921
  if(deleteBtn){deleteBtn.disabled=true;deleteBtn.onclick=null;}
 
939
  const activeDetail={...selected,...(state.activeRunProgress||{}),summary:{...(selected.summary||{}),...(state.activeRunProgress?.summary||{})}};
940
  applyValidationPrefill(activeDetail,'active run',{preserveSelection:false});
941
  }
942
+ function contextualizeSpaceTestFromBuildRun(detail={}){
943
+ if(!detail||typeof detail!=='object')return;
944
+ if(typeof isValidationRunDetail==='function'&&isValidationRunDetail(detail))return;
945
+ if(state.validationMode==='running')return;
946
+ const prefill=validationPrefillFromDetail(detail);
947
+ if(!prefill.parentBuildRunId||!prefill.target||prefill.target==='—')return;
948
+ state.validationPrefillDraft={...prefill,sourceLabel:'selected Build Run',parentBuildRunId:prefill.parentBuildRunId,sourceRunId:prefill.parentBuildRunId,linkedTargetSpace:prefill.target};
949
+ state.linkedValidationParent=prefill.parentBuildRunId;
950
+ if(!state.validationArgsDirty){
951
+ setInputValue('validateSpace',prefill.target);
952
+ setInputValue('expectedOutput',prefill.expected);
953
+ setInputValue('validateApi',prefill.api);
954
+ setInputValue('testArgs',prettyJson(prefill.args));
955
+ setInputValue('testKwargs',prettyJson(prefill.kwargs));
956
+ }
957
+ const endpoints=$('spaceTestEndpoints');
958
+ if(endpoints&&state.centerTab!=='validate')endpoints.innerHTML=`<div class="empty compact-empty">Linked to selected Build Run <code>${escapeHtml(prefill.parentBuildRunId)}</code>. ${prefill.requiresEndpointDiscovery?'Endpoints will be discovered before validation.':'Open Space Test to replay or run a linked validation.'}</div>`;
959
+ setValidationLaunchEnabled();
960
+ }
961
  function prepareValidationFromSelectedRun(){
962
  applyValidationPrefill(state.selectedRunDetail||{},'selected run',{preserveSelection:false});
963
  }
964
  async function launchBuild(){const formError=validateBuildForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching build Job…');setButtonBusy('launchBuild',true,'Launching…');try{setText('hardwarePlan',hardwarePlanLabel());const payload={bucket_name:state.bucketName,model_id:formValue('modelId'),target_space_name:formValue('targetSpace')||undefined,pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.checked??true,try_zero_gpu_first:$('tryZeroGpu')?.checked??true,expected_output_type:formValue('buildExpectedOutput')};const result=await apiPost('/api/build',payload);setActiveRun(result);showMessage(`Build launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchBuild',false)}}
965
+ async function launchValidate(){const formError=validateValidationForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching validation Job…');setButtonBusy('launchValidate',true,'Launching…');try{const args=parseJsonField('testArgs',[]);const kwargs=parseJsonField('testKwargs',{});const draft=state.validationPrefillDraft||{};const apiForLaunch=draft.requiresEndpointDiscovery?'':normalizeApiName(formValue('validateApi')||'/generate');const payload={bucket_name:state.bucketName,parent_build_run_id:draft.parentBuildRunId||state.linkedValidationParent,target_space_id:formValue('validateSpace'),api_name:apiForLaunch,expected_output_type:formValue('expectedOutput')||'any',test_args_json:prettyJson(args),test_kwargs_json:prettyJson(kwargs),live_timeout_seconds:validationTimeoutValue()};const result=await apiPost('/api/validate',payload);state.validationPrefillDraft=null;state.validationArgsDirty=false;state.validationMode='running';renderValidationLaunchResult(result);startValidationPolling(result.run_id);showMessage(`Validation launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchValidate',false)}}
966
  async function refreshRunReportPreview(runId){
967
  if(!runId)return;
968
  try{
 
1008
  const latency=firstPositiveNumber(smoke.latency_seconds,smoke.observed_latency_seconds,summary.latency_seconds,summary.observed_latency_seconds,p.latency_seconds,p.observed_latency_seconds,manual.latency_seconds,manual.observed_latency_seconds,eventLatency);
1009
  const duration=firstPositiveNumber(smoke.recommended_zerogpu_duration_seconds,smoke.recommended_zero_gpu_duration_seconds,smoke.recommended_duration_seconds,summary.recommended_zerogpu_duration_seconds,summary.recommended_zero_gpu_duration_seconds,summary.recommended_duration_seconds,manual.recommended_zero_gpu_duration_seconds,gate.recommended_zerogpu_duration_seconds,gate.recommended_zero_gpu_duration_seconds,gate.zero_gpu_duration_recommendation,eventDuration);
1010
  const source=smoke.recommendation_source||summary.recommendation_source||manual.recommendation_source||gate.recommendation_source||(manual.status==='success'?'linked Space Test':'')||(eventLatency?'generation_smoke event':'');
1011
+ const hardware=manual.hardware_used_for_validation||manual.recommendation_hardware||summary.hardware_used_for_validation||summary.recommendation_hardware||p.hardware_used_for_validation||p.recommendation_hardware||p.state?.selected_hardware||summary.selected_hardware||'';
1012
  const failureType=smoke.failure_type||validation.failure_type||gate.failure_type||'';
1013
  const failureOwner=smoke.failure_owner||validation.failure_owner||gate.failure_owner||'';
1014
  const missingArgument=smoke.missing_argument||validation.missing_argument||gate.missing_argument||'';
1015
  const error=smoke.error||smoke.message||validation.failure_reason||gate.message||'';
1016
+ return {smoke,gate,latency,duration,source,hardware,failureType,failureOwner,missingArgument,error};
1017
  }
1018
  function smokeFailureLatencyNote({smoke={},failureType='',failureOwner='',missingArgument='',error=''}={}){
1019
  const status=String(smoke.status||'').toLowerCase();
 
1038
  const runId=String(p.run_id||p.summary?.run_id||state.runId||'');
1039
  state.latencyCache=state.latencyCache||{};
1040
  const payload=readRunLatencyPayload(p);
1041
+ const {latency,duration,source,hardware}=payload;
1042
  const hasLatency=Number.isFinite(latency)&&latency>0;
1043
  const hasDuration=Number.isFinite(duration)&&duration>0;
1044
  const cached=state.latencyCache[runId]||{};
 
1048
  const finalBlocker=p.build_error_observation?' Final blocker: build error after repair.':'';
1049
  const failedWithoutSmoke=(runStatus==='failed'||runStatus==='error')&&!hasLatency;
1050
  const failureNote=smokeFailureLatencyNote(payload)||(failedWithoutSmoke?`Not measured because live validation did not reach a successful generation smoke test.${finalBlocker}`:'');
1051
+ const hardwareSuffix=hardware&&String(hardware).toLowerCase()!=='unknown'?` on ${hardware}`:'';
1052
+ const stableSource=hasLatency?`${source||'live smoke test'}${hardwareSuffix}`:(failureNote?'not measured: smoke failed':(cached.source||'measured when smoke test passes'));
1053
  if(hasLatency||hasDuration)state.latencyCache[runId]={latency:stableLatency,duration:stableDuration,source:stableSource};
1054
  const finalHasLatency=Number.isFinite(stableLatency)&&stableLatency>0;
1055
  const finalHasDuration=Number.isFinite(stableDuration)&&stableDuration>0;
 
1059
  const note=$('activeLatencyNote');
1060
  if(note){
1061
  note.textContent=finalHasDuration
1062
+ ? `This measured latency is normally used to tune the \`@spaces.GPU(duration=...)\` decorator for ZeroGPU when the generated Space uses ZeroGPU. Source hardware: ${hardware||'not recorded'}.`
1063
  : (failureNote||'When available, the live smoke-test latency is used to recommend the `@spaces.GPU(duration=...)` value for ZeroGPU apps.');
1064
  }
1065
  card.classList.toggle('has-latency',finalHasLatency);
 
1120
  el.classList.toggle('is-pending',pending);
1121
  });
1122
  }
1123
+ function renderMetrics(p){const root=$('metricsList');const {smoke,gate,latency,duration,source,hardware}=readRunLatencyPayload(p);const health=(gate.implementation_signals&&gate.implementation_signals.health_passed)||smoke.health_passed;const smokeOk=smoke.ok||smoke.status==='success';renderActiveLatency(p);if(!root)return;const provenance=[source||'',hardware?`hardware ${hardware}`:''].filter(Boolean).join(' · ');root.innerHTML=`<li><span>/health</span><strong class="${health?'metric-ok':''}">${health?'passed':'pending'}</strong></li><li><span>/generate smoke test</span><strong class="${smokeOk?'metric-ok':'metric-warn'}">${smokeOk?'passed':(smoke.status||'pending')}</strong></li><li><span>Latency</span><strong>${Number.isFinite(latency)&&latency>0?formatLatencySeconds(latency):'—'}</strong></li><li><span>Recommended ZeroGPU duration</span><strong>${Number.isFinite(duration)&&duration>0?fmtSeconds(Math.round(duration)):'—'}</strong>${provenance?`<em>${escapeHtml(provenance)}</em>`:''}</li>`}
1124
  function renderEvents(events){const root=$('eventsPanel');if(!root)return;try{const rows=Array.isArray(events)?events:[];root.textContent=rows.slice(-12).map(e=>`${e?.ts||''} ${e?.step||''} ${e?.status||''} ${e?.message||''}`).join('\n')||'No events yet. The run may still be scheduling.'}catch(e){console.warn('renderEvents failed',e);root.textContent='Events could not be rendered.'}}
1125
  async function cancelActiveRun(){
1126
  if(!state.runId){showMessage('Select a running run before cancelling.', 'warning');return}
 
1178
  enableEvalArchive:enableEvalArchive,
1179
  verifyEvalArchive:verifyEvalArchive,
1180
  disableEvalArchive:disableEvalArchive,
1181
+ flushEvalArchive:flushEvalArchive,
1182
  refreshRuns:()=>loadRuns(true),
1183
  refreshRunsHome:()=>loadRuns(true),
1184
  cancelRun:cancelActiveRun,
 
1202
  // 'Eval archive off · set env vars'
1203
  // Anonymous project eval archive enabled:
1204
  // Env override remains supported for backend publishing: ASF_EVAL_ENABLED=true, ASF_EVAL_BUCKET_SOURCE=<namespace>/<bucket>, ASF_EVAL_BUCKET_PATH=evals, ASF_EVAL_BUCKET_MOUNT=/evals.
1205
+ /* v190.19 legacy invariant strings:
1206
+ setText('spaceTestOutput',prefill.automaticSmokePassed?'Replay linked smoke test':'New linked validation')
1207
+ parent_build_run_id:(state.validationPrefillDraft||{}).parentBuildRunId
1208
+ */
web/static/progress.js CHANGED
@@ -929,13 +929,16 @@ function renderDiagnostics(d={},header={}){
929
  if(rules)rules.innerHTML=(d.zerogpu_rules||[]).map(r=>`<li class="${statusClass(r.status)}"><span>${escapeHtml(r.label)}</span><strong>${escapeHtml(r.status||'pending')}</strong></li>`).join('');
930
  }
931
  function renderSpaceTestPreview(s={}){
 
 
932
  const st=$('spaceTestStatus');
933
  if(st){
934
- st.textContent=badgeLabel(s.status||'pending');
935
- st.className='badge '+statusClass(s.status);
936
  }
937
- setText('spaceTestOutput',s.output_artifact||s.endpoint||'Waiting for API test');
938
- setText('spaceTestVerdict',s.verdict||'pending');
 
939
  }
940
  function animateProgressBar(progress,status,currentStep){
941
  const fill=$('progressFill');
 
929
  if(rules)rules.innerHTML=(d.zerogpu_rules||[]).map(r=>`<li class="${statusClass(r.status)}"><span>${escapeHtml(r.label)}</span><strong>${escapeHtml(r.status||'pending')}</strong></li>`).join('');
930
  }
931
  function renderSpaceTestPreview(s={}){
932
+ const policy=s.policy||s.space_test_policy||{};
933
+ const status=policy.mode||s.status||'pending';
934
  const st=$('spaceTestStatus');
935
  if(st){
936
+ st.textContent=policy.label||badgeLabel(status);
937
+ st.className='badge '+(policy.enabled?statusClass(status):'neutral');
938
  }
939
+ const endpointLabel=policy.requires_endpoint_discovery?'Endpoint discovery required':(s.output_artifact||s.endpoint||policy.endpoint||'Waiting for API test');
940
+ setText('spaceTestOutput',endpointLabel);
941
+ setText('spaceTestVerdict',policy.message||s.verdict||'pending');
942
  }
943
  function animateProgressBar(progress,status,currentStep){
944
  const fill=$('progressFill');
web/static/runs.js CHANGED
@@ -1,7 +1,11 @@
 
 
 
1
  function inferredOwnerFromBucket(bucket){return bucket?String(bucket).split('/')[0]:''}
2
  function inferredJobUrl(summary={}, detail={}){if(summary.job_url)return summary.job_url;const launch=detail.launch||{};const stateObj=detail.state||{};if(launch.job_url)return launch.job_url;if(stateObj.job_url)return stateObj.job_url;const jobId=summary.job_id||launch.job_id||stateObj.job_id;const owner=summary.created_by||summary.username||summary.owner||launch.created_by||launch.username||launch.owner||stateObj.created_by||stateObj.username||stateObj.owner||state.user?.username||inferredOwnerFromBucket(summary.bucket_source||detail.bucket_source||state.bucketSource);return jobId&&owner?`https://huggingface.co/jobs/${owner}/${jobId}`:''}
3
  function bucketRunFileUrl(bucket,runId,relPath=''){const rel=String(relPath||'').replace(/^\/+|\/+$/g,'');return bucket&&runId?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}${rel?`/${rel}`:''}`:''}
4
  function runValidationTerminalText(run={}){
 
5
  return [
6
  run.verdict,run.validation_status,run.result_status,run.final_status,run.status,run.summary?.verdict,run.summary?.validation_status,run.summary?.result_status,run.summary?.status,
7
  run.state?.verdict,run.state?.validation_status,run.state?.result_status,run.state?.status,run.gate_status,run.summary?.gate_status,run.inference_gate?.status,
@@ -9,11 +13,13 @@ function runValidationTerminalText(run={}){
9
  ].map(x=>String(x||'').toLowerCase()).filter(Boolean).join(' ');
10
  }
11
  function validationLockedStatus(run={}){
 
12
  const runId=run.run_id||run.summary?.run_id||run.state?.run_id||'';
13
  const terminal=runId&&typeof state!=='undefined'&&state.validationTerminalByRun?state.validationTerminalByRun[runId]:null;
14
  return terminal?.canonical||'';
15
  }
16
  function runCanonicalStatus(run={}){
 
17
  const validation=isValidationRunSummary(run);
18
  const locked=validation?validationLockedStatus(run):'';
19
  if(locked)return locked;
@@ -32,7 +38,7 @@ function runCanonicalStatus(run={}){
32
  if(text.includes('running')||text.includes('queued')||text.includes('pending')||text.includes('started')||text.includes('waiting')||text.includes('building'))return'running';
33
  return s||'unknown';
34
  }
35
- function runMatchesStatus(run,status){const canon=runCanonicalStatus(run);if(!status||status==='all')return true;if(status==='failed')return canon==='failed';if(status==='running')return canon==='running';if(status==='full_inference_success')return canon==='full_inference_success';if(status==='partial_validation')return canon==='partial_validation';if(status==='manual_hardware_required')return canon==='manual_hardware_required';if(status==='technical_blocker')return canon==='technical_blocker';if(status==='stale')return canon==='stale';return canon===String(status).toLowerCase()}
36
  function canonicalStatusForDeletion(context={}){
37
  const kind=context.kind==='validation'?'validation':'active';
38
  const detail=context.detail||{};
@@ -41,8 +47,9 @@ function canonicalStatusForDeletion(context={}){
41
  if(kind==='validation'&&typeof runCanonicalStatus==='function')return runCanonicalStatus(merged);
42
  return String(merged.status||merged.summary?.status||merged.state?.status||'unknown').toLowerCase();
43
  }
44
- function runMatchesQuery(run,query){if(!query)return true;const haystack=[run.run_id,run.model_id,run.target_space,run.status,run.kind,run.selected_hardware].map(x=>String(x||'').toLowerCase()).join(' ');return haystack.includes(String(query).toLowerCase())}
45
  function runSortTimestamp(run={}){
 
46
  const raw=run.updated_at||run.created_at||run.started_at||run.cancelled_at||'';
47
  const ts=Date.parse(raw);
48
  if(Number.isFinite(ts))return ts;
@@ -55,7 +62,7 @@ function runSortTimestamp(run={}){
55
  return 0;
56
  }
57
  function sortRunsNewestFirst(runs=[]){
58
- return [...(runs||[])].sort((a,b)=>runSortTimestamp(b)-runSortTimestamp(a)||String(b.run_id||'').localeCompare(String(a.run_id||'')));
59
  }
60
  function filteredRuns(){
61
  const query=state.runSearch||'';
@@ -92,6 +99,7 @@ function runCompactStatusLabel(status){
92
  const s=String(status||'unknown').toLowerCase();
93
  if(s==='full_inference_candidate_health_passed'||s==='health_only'||s==='partial_validation'||s==='partial'||s==='completed_with_warnings')return 'Partial';
94
  if(s==='validated_after_manual_space_test'||s==='manual_validation_passed'||s==='validated')return 'Validated';
 
95
  if(s.includes('success')||s.includes('succeed')||s.includes('passed')||s==='done'||s==='completed')return 'Success';
96
  if(s.includes('running')||s.includes('queued')||s.includes('pending')||s.includes('building')||s.includes('waiting'))return 'Running';
97
  if(s.includes('manual')||s.includes('hardware'))return 'Manual';
@@ -160,12 +168,14 @@ function runSecondaryLine(run={}){
160
  const manual=buildManualValidationStatus(run);
161
  const manualText=manual.passed?'Manual validation passed':manual.failed?'Manual validation attempted':'';
162
  const linkedText=manual.linkedCount?`${manual.linkedCount} linked test${manual.linkedCount===1?'':'s'}`:'';
163
- const parts=[manualText,linkedText,run.target_space,mode?'Goal: '+implementationModeLabel(mode,{short:true}):'',run.selected_hardware].filter(Boolean);
 
 
164
  return parts.join(' • ');
165
  }
166
  function runCardMeta(run={}){return runSecondaryLine(run)}
167
  function renderRunRows(runs,compact=false){
168
- return (runs||[]).map(r=>{
169
  const validation=isValidationRunSummary(r);
170
  const kind=runCardKind(r);
171
  // const status=runCanonicalStatus(r)||r.status||'unknown'
@@ -219,16 +229,34 @@ function renderRunPagination(total){
219
  root.innerHTML=`<button class="secondary small" data-page-action="prev" ${state.runPage<=0?'disabled':''}>‹</button><span>Page ${state.runPage+1} / ${pages}</span><button class="secondary small" data-page-action="next" ${state.runPage>=pages-1?'disabled':''}>›</button>`;
220
  }
221
 
222
- function isValidationRunSummary(run={}){return isValidationRunDetail({summary:run,state:run,kind:run.kind,run_type:run.run_type})}
 
 
223
 
224
  function linkedValidationParentId(run={}){
 
225
  return run.parent_build_run_id||run.parentBuildRunId||run.summary?.parent_build_run_id||run.state?.parent_build_run_id||'';
226
  }
227
  function linkedValidationsForBuild(buildRunId){
228
  if(!buildRunId)return [];
229
- return (state.runsCache||[]).filter(r=>isValidationRunSummary(r)&&linkedValidationParentId(r)===buildRunId);
230
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  function buildManualValidationStatus(run={}){
 
232
  const explicit=run.manual_validation_status||run.summary?.manual_validation_status||{};
233
  const linked=linkedValidationsForBuild(run.run_id).filter(v=>!runMatchesStatus(v,'running'));
234
  const passed=Boolean(run.manual_validation_passed||run.summary?.manual_validation_passed||explicit.status==='success'||linked.some(v=>runMatchesStatus(v,'full_inference_success')));
@@ -237,21 +265,25 @@ function buildManualValidationStatus(run={}){
237
  return {passed,failed,linkedCount:linked.length,latest,explicit};
238
  }
239
  function buildEffectiveStatus(run={}){
 
240
  const manual=buildManualValidationStatus(run);
241
  const eff=String(run.effective_status||run.summary?.effective_status||manual.explicit.effective_status||'').toLowerCase();
242
  if(eff)return eff;
 
243
  if(manual.passed)return 'validated_after_manual_space_test';
244
  return runCanonicalStatus(run);
245
  }
246
  function explorerPrimaryRuns(){
247
- return (state.runsCache||[]).filter(r=>!isValidationRunSummary(r));
248
  }
249
- function runStatTime(run={}){const raw=run.updated_at||run.created_at||run.started_at||run.cancelled_at||'';return raw?formatTime(raw):'—'}
250
  function runStatLatency(run={}){
 
251
  const latency=run.latency_seconds||run.observed_latency_seconds||run.generation_smoke?.latency_seconds||run.generation_smoke?.observed_latency_seconds;
252
  return latency?fmtSeconds(Number(latency)):'';
253
  }
254
  function runStatLatencyValue(run={}){
 
255
  const latency=run.latency_seconds||run.observed_latency_seconds||run.generation_smoke?.latency_seconds||run.generation_smoke?.observed_latency_seconds;
256
  const n=Number(latency);
257
  return Number.isFinite(n)&&n>0?n:null;
@@ -266,12 +298,13 @@ function percentile(values=[],p=0.5){
266
  }
267
  function pct(part,total){return total?`${Math.round((part/total)*100)}%`:'—'}
268
  function runStatsIssueCounts(runs=[]){
 
269
  const counts={failed_builds:0,failed_linked_tests:0,manual_hardware:0,pi_model_mismatch:0,stale:0};
270
  for(const r of runs){
271
  const validation=isValidationRunSummary(r);
272
  const status=String(r.status||'').toLowerCase();
273
  const pi=r.pi_model_resolution||{};
274
- if(runMatchesStatus(r,'failed')) validation?counts.failed_linked_tests++:counts.failed_builds++;
275
  if(!validation&&runMatchesStatus(r,'manual_hardware_required')) counts.manual_hardware++;
276
  if(!validation&&(pi.mismatch||status.includes('model_mismatch'))) counts.pi_model_mismatch++;
277
  if(status.includes('stale')) counts.stale++;
@@ -284,15 +317,17 @@ function runStatsCell(label,value,detail='',tone='neutral'){
284
  function renderRunAggregateStats(){
285
  const root=$('runStatsList');
286
  if(!root)return;
287
- const runs=state.runsCache||[];
288
  const builds=runs.filter(r=>!isValidationRunSummary(r));
289
- const validations=runs.filter(r=>isValidationRunSummary(r));
290
  const runningBuilds=builds.filter(r=>runMatchesStatus(r,'running')).length;
291
  const failedBuilds=builds.filter(r=>runMatchesStatus(r,'failed')).length;
292
  const terminalBuilds=builds.filter(r=>!runMatchesStatus(r,'running'));
293
  const automaticSuccess=terminalBuilds.filter(r=>runMatchesStatus(r,'full_inference_success')).length;
294
  const successBuilds=automaticSuccess;
295
  const effectiveSuccess=terminalBuilds.filter(r=>runMatchesStatus({...r,status:buildEffectiveStatus(r)},'full_inference_success')).length;
 
 
296
  // Legacy aggregate labels kept for regression tests: Total runs / Test response time / Typical / slowest recent / Build success / Validation pass.
297
  // Legacy formula kept for audit readability: pct(successBuilds,terminalBuilds.length) and pct(passedValidations,terminalValidations.length).
298
  const passedLinked=validations.filter(r=>!runMatchesStatus(r,'running')&&runMatchesStatus(r,'full_inference_success')).length;
@@ -325,6 +360,7 @@ function renderRunAggregateStats(){
325
  ${runStatsCell('Build status',`${runningBuilds} running`,`${failedBuilds} failed` ,failedBuilds?'warn':runningBuilds?'running':'success')}
326
  ${runStatsCell('Automatic success',pct(automaticSuccess,terminalBuilds.length),`${automaticSuccess}/${terminalBuilds.length} finished builds`,'success')}
327
  ${runStatsCell('Effective success',pct(effectiveSuccess,terminalBuilds.length),`${effectiveSuccess}/${terminalBuilds.length} after linked tests`,'success')}
 
328
  </div>
329
  <div class="run-stats-band linked-tests-band">
330
  <span>Linked Space Tests</span>
@@ -358,11 +394,12 @@ function forgetRunEverywhere(runId){
358
  }
359
  if(state.documentCache)delete state.documentCache[runId];
360
  if(state.latencyCache)delete state.latencyCache[runId];
361
- state.runsCache=(state.runsCache||[]).filter(r=>r.run_id!==runId);
 
362
  document.querySelectorAll(`.run-row[data-run="${CSS.escape(runId)}"]`).forEach(row=>row.remove());
363
  }
364
  function activeRunIds(){
365
- return new Set((state.runsCache||[]).map(r=>r.run_id).filter(Boolean));
366
  }
367
 
368
  function associatedSpaceFromDetail(detail={},summaryOverride={}){
@@ -409,7 +446,7 @@ function bindRunRowEvents(root){
409
  });
410
  }
411
  function renderRunsFromCache(){
412
- state.runsCache=sortRunsNewestFirst(state.runsCache||[]);
413
  const runs=filteredRuns();
414
  const perPage=state.runsPerPage||5;
415
  state.runPage=Math.min(Math.max(0,state.runPage||0),Math.max(0,Math.ceil(runs.length/perPage)-1));
@@ -431,7 +468,7 @@ function renderRunsFromCache(){
431
 
432
  function mergeRunExplorerTerminalLocks(runs=[]){
433
  if(typeof state==='undefined'||!state.validationTerminalByRun)return runs||[];
434
- return (runs||[]).map(run=>{
435
  const validation=isValidationRunSummary(run);
436
  const runId=run.run_id||run.summary?.run_id||'';
437
  const terminal=validation&&runId?state.validationTerminalByRun[runId]:null;
@@ -439,10 +476,11 @@ function mergeRunExplorerTerminalLocks(runs=[]){
439
  return {...run,status:terminal.canonical,validation_status:terminal.canonical,result_status:terminal.canonical,summary:{...(run.summary||{}),status:terminal.canonical,validation_status:terminal.canonical,result_status:terminal.canonical}};
440
  });
441
  }
442
- async function loadRuns(forceNetwork=false){const stale=Date.now()-(state.runsLastLoaded||0)>45000;const shouldFetch=forceNetwork||!state.runsCache.length||stale;if(!shouldFetch){renderRunsFromCache();return state.runsCache||[]}if(state.runsLoadPromise&&!forceNetwork)return state.runsLoadPromise;document.querySelectorAll('.runs-panel,.full-runs-panel').forEach(x=>x.classList.add('is-refreshing'));state.runsLoadPromise=apiGet(`/api/runs?limit=100&bucket_name=${encodeURIComponent(state.bucketName)}`).then(data=>{state.runsCache=sortRunsNewestFirst(mergeRunExplorerTerminalLocks((data.runs||[]).filter(r=>!isRunLocallyDeleted(r.run_id))));state.runsLastLoaded=Date.now();if(data.bucket_source)state.bucketSource=data.bucket_source;renderRunsFromCache();return state.runsCache}).catch(e=>{for(const id of ['runsTable','runsTableHome']){const root=$(id);if(root)root.textContent='Could not load runs: '+e.message}if(forceNetwork)console.warn(e);return state.runsCache||[]}).finally(()=>{state.runsLoadPromise=null;document.querySelectorAll('.runs-panel,.full-runs-panel').forEach(x=>x.classList.remove('is-refreshing'))});return state.runsLoadPromise}
 
443
  function cachedRunById(runId){
444
  if(isRunLocallyDeleted(runId))return null;
445
- return (state.runsCache||[]).find(r=>r.run_id===runId)||null;
446
  }
447
  function optimisticDetailFromSummary(summary={}){
448
  if(!summary)return null;
@@ -506,13 +544,13 @@ function upsertRunExplorerFromProgress(p={}){
506
  effective_status:p.summary?.effective_status||p.manual_validation_status?.effective_status,
507
  bucket_source:p.bucket_source||p.summary?.bucket_source||state.bucketSource,
508
  };
509
- state.runsCache=state.runsCache||[];
510
- const idx=state.runsCache.findIndex(r=>r.run_id===runId);
511
  if(idx>=0)state.runsCache[idx]={...state.runsCache[idx],...summary};
512
  else state.runsCache.unshift(summary);
513
  const parentId=summary.parent_build_run_id||linkedValidationParentId(summary);
514
  if(parentId&&isValidationRunSummary(summary)){
515
- const parentIdx=state.runsCache.findIndex(r=>r.run_id===parentId);
516
  if(parentIdx>=0){
517
  const passed=runMatchesStatus(summary,'full_inference_success');
518
  state.runsCache[parentIdx]={...state.runsCache[parentIdx],manual_validation_passed:state.runsCache[parentIdx].manual_validation_passed||passed,effective_status:passed?'validated_after_manual_space_test':state.runsCache[parentIdx].effective_status,updated_at:summary.updated_at||state.runsCache[parentIdx].updated_at};
@@ -733,12 +771,13 @@ function confirmRunDeletion({runId,kind='active',status='',detail={}}={}){
733
  const label=kind==='validation'?'validation run':'build run';
734
  const associatedSpace=associatedSpaceFromDetail(detail,detail.summary||{});
735
  const canDeleteSpace=runCanOfferSpaceDeletion(kind,detail,detail.summary||{});
 
736
  kicker.textContent=kind==='validation'?'Space Test deletion':'Build run deletion';
737
  title.textContent=`Delete this ${label}?`;
738
  body.textContent=kind==='validation'
739
- ? 'This removes only this validation run’s bucket artifacts, logs, state, traces and report. The tested Space will not be deleted.'
740
- : 'This removes its bucket artifacts, logs, state, traces and report. The generated Space is kept unless you explicitly choose to delete it below.';
741
- meta.innerHTML=`<span><strong>Run</strong><code>${escapeHtml(runId||'—')}</code></span><span><strong>Type</strong>${escapeHtml(kind==='validation'?'Space Test':'Build')}</span><span><strong>Status</strong>${escapeHtml(badgeLabel(normalized))}</span>${associatedSpace?`<span><strong>Space</strong><code>${escapeHtml(associatedSpace)}</code></span>`:''}`;
742
  if(spaceOption&&spaceCheckbox&&spaceLabel){
743
  spaceCheckbox.checked=false;
744
  spaceOption.hidden=!canDeleteSpace;
@@ -768,7 +807,7 @@ function confirmRunDeletion({runId,kind='active',status='',detail={}}={}){
768
  const setDeleting=()=>{
769
  modal.classList.add('is-deleting');
770
  title.textContent=kind==='validation'?'Deleting this validation run…':'Deleting this run…';
771
- body.textContent='Removing bucket artifacts. The panel will reset as soon as deletion is confirmed.';
772
  confirmBtn.textContent='Deleting…';
773
  confirmBtn.disabled=true;
774
  cancelEls.forEach(el=>{el.disabled=true;el.setAttribute('aria-disabled','true')});
@@ -779,8 +818,8 @@ function confirmRunDeletion({runId,kind='active',status='',detail={}}={}){
779
  submitted=false;
780
  title.textContent=`Delete this ${label}?`;
781
  body.textContent=kind==='validation'
782
- ? 'This removes only this validation run’s bucket artifacts, logs, state, traces and report. The tested Space will not be deleted.'
783
- : 'This removes its bucket artifacts, logs, state, traces and report. The generated Space is kept unless you explicitly choose to delete it below.';
784
  confirmBtn.textContent=active?'Delete stored files':'Delete run';
785
  confirmBtn.disabled=false;
786
  cancelEls.forEach(el=>{el.disabled=false;el.removeAttribute('aria-disabled')});
@@ -815,12 +854,16 @@ async function deleteRunById(runId,options={}){
815
  let success=false;
816
  try{
817
  const result=await apiDelete(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}`,deleteSpace?{delete_space:true}:{});
 
 
818
  const suffix=deleteSpace?(result.space_deleted?' and associated Space':(result.space_delete_error?' but Space deletion failed':' and Space was already missing')):'';
819
- showMessage(`Deleted run ${runId}${suffix}`,'success');
820
  success=true;
821
  rememberDeletedRun(runId);
 
822
  clearDeletedRunState(runId,context.kind);
823
  forgetRunEverywhere(runId);
 
824
  renderRunsFromCache();
825
  if(confirmed&&typeof confirmed.close==='function')confirmed.close(true);
826
  loadRuns(true).catch(e=>console.warn('post-delete runs refresh failed',e));
@@ -1192,12 +1235,16 @@ function renderEvalArchiveStatus(detail={}){
1192
  const path=status.archive_relative_path||status.path||'';
1193
  const localLabel=(status.local_record_found||Object.keys(local||{}).length)?'written':'pending';
1194
  const backendLabel=published?'published':attempted&&reason==='record_not_ready'?'pending':attempted?'not published':'pending';
 
 
1195
  const intro=published?'Eval record copied to the operator archive bucket.':attempted&&reason==='record_not_ready'?'Eval record will be archived when the run completes.':'Archive publication is not confirmed yet.';
1196
- body.innerHTML=`<p>${escapeHtml(intro)}</p><div class="eval-archive-grid"><div><span>Local record</span><strong>${escapeHtml(localLabel)}</strong></div><div><span>Backend copy</span><strong>${escapeHtml(backendLabel)}</strong></div><div><span>Mode</span><strong>${escapeHtml(status.publish_mode||status.mode||'backend')}</strong></div><div><span>Archive bucket</span><strong>${escapeHtml(bucket||'—')}</strong></div></div>${path?`<code>${escapeHtml(path)}</code>`:''}${reason&&!published&&reason!=='record_not_ready'?`<p class="eval-warning">${escapeHtml(reason)}</p>`:''}`;
 
1197
  }
1198
  function renderRunDetail(detail){
1199
  state.selectedRunDetail=detail;
1200
  const summary=detail.summary||{};
 
1201
  const smoke=detail.generation_smoke||{};
1202
  const files=detail.files||[];
1203
  const view=detail.view||{};
@@ -1248,3 +1295,5 @@ function renderRunDetail(detail){
1248
  // Legacy clean-delete invariant: await loadRuns(true) was intentionally replaced by post-delete background refresh in v190.9.
1249
 
1250
  // Legacy validation preview invariant: if(isValidationRunDetail(detail)){showValidationRunDetail(detail)
 
 
 
1
+ function safeRunObject(run){return run&&typeof run==='object'?run:{}}
2
+ function validRunObject(run){return Boolean(run&&typeof run==='object'&&(run.run_id||run.summary?.run_id||run.state?.run_id));}
3
+ function validRunsList(runs=[]){return (Array.isArray(runs)?runs:[]).filter(validRunObject);}
4
  function inferredOwnerFromBucket(bucket){return bucket?String(bucket).split('/')[0]:''}
5
  function inferredJobUrl(summary={}, detail={}){if(summary.job_url)return summary.job_url;const launch=detail.launch||{};const stateObj=detail.state||{};if(launch.job_url)return launch.job_url;if(stateObj.job_url)return stateObj.job_url;const jobId=summary.job_id||launch.job_id||stateObj.job_id;const owner=summary.created_by||summary.username||summary.owner||launch.created_by||launch.username||launch.owner||stateObj.created_by||stateObj.username||stateObj.owner||state.user?.username||inferredOwnerFromBucket(summary.bucket_source||detail.bucket_source||state.bucketSource);return jobId&&owner?`https://huggingface.co/jobs/${owner}/${jobId}`:''}
6
  function bucketRunFileUrl(bucket,runId,relPath=''){const rel=String(relPath||'').replace(/^\/+|\/+$/g,'');return bucket&&runId?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}${rel?`/${rel}`:''}`:''}
7
  function runValidationTerminalText(run={}){
8
+ run=safeRunObject(run);
9
  return [
10
  run.verdict,run.validation_status,run.result_status,run.final_status,run.status,run.summary?.verdict,run.summary?.validation_status,run.summary?.result_status,run.summary?.status,
11
  run.state?.verdict,run.state?.validation_status,run.state?.result_status,run.state?.status,run.gate_status,run.summary?.gate_status,run.inference_gate?.status,
 
13
  ].map(x=>String(x||'').toLowerCase()).filter(Boolean).join(' ');
14
  }
15
  function validationLockedStatus(run={}){
16
+ run=safeRunObject(run);
17
  const runId=run.run_id||run.summary?.run_id||run.state?.run_id||'';
18
  const terminal=runId&&typeof state!=='undefined'&&state.validationTerminalByRun?state.validationTerminalByRun[runId]:null;
19
  return terminal?.canonical||'';
20
  }
21
  function runCanonicalStatus(run={}){
22
+ run=safeRunObject(run);
23
  const validation=isValidationRunSummary(run);
24
  const locked=validation?validationLockedStatus(run):'';
25
  if(locked)return locked;
 
38
  if(text.includes('running')||text.includes('queued')||text.includes('pending')||text.includes('started')||text.includes('waiting')||text.includes('building'))return'running';
39
  return s||'unknown';
40
  }
41
+ function runMatchesStatus(run,status){const canon=runCanonicalStatus(run);const effective=String(run?.effective_status||run?.summary?.effective_status||run?.manual_validation_status?.effective_status||'').toLowerCase();if(!status||status==='all')return true;if(status==='failed')return canon==='failed';if(status==='running')return canon==='running';if(status==='full_inference_success')return canon==='full_inference_success'||effective==='validated_after_manual_space_test'||effective==='manual_validation_passed'||effective==='validated'||effective==='recovered_by_manual_validation';if(status==='partial_validation')return canon==='partial_validation';if(status==='manual_hardware_required')return canon==='manual_hardware_required';if(status==='technical_blocker')return canon==='technical_blocker';if(status==='stale')return canon==='stale';return canon===String(status).toLowerCase()||effective===String(status).toLowerCase()}
42
  function canonicalStatusForDeletion(context={}){
43
  const kind=context.kind==='validation'?'validation':'active';
44
  const detail=context.detail||{};
 
47
  if(kind==='validation'&&typeof runCanonicalStatus==='function')return runCanonicalStatus(merged);
48
  return String(merged.status||merged.summary?.status||merged.state?.status||'unknown').toLowerCase();
49
  }
50
+ function runMatchesQuery(run,query){run=safeRunObject(run);if(!query)return true;const haystack=[run.run_id,run.model_id,run.target_space,run.status,run.kind,run.selected_hardware].map(x=>String(x||'').toLowerCase()).join(' ');return haystack.includes(String(query).toLowerCase())}
51
  function runSortTimestamp(run={}){
52
+ run=safeRunObject(run);
53
  const raw=run.updated_at||run.created_at||run.started_at||run.cancelled_at||'';
54
  const ts=Date.parse(raw);
55
  if(Number.isFinite(ts))return ts;
 
62
  return 0;
63
  }
64
  function sortRunsNewestFirst(runs=[]){
65
+ return [...validRunsList(runs)].sort((a,b)=>runSortTimestamp(b)-runSortTimestamp(a)||String(b.run_id||'').localeCompare(String(a.run_id||'')));
66
  }
67
  function filteredRuns(){
68
  const query=state.runSearch||'';
 
99
  const s=String(status||'unknown').toLowerCase();
100
  if(s==='full_inference_candidate_health_passed'||s==='health_only'||s==='partial_validation'||s==='partial'||s==='completed_with_warnings')return 'Partial';
101
  if(s==='validated_after_manual_space_test'||s==='manual_validation_passed'||s==='validated')return 'Validated';
102
+ if(s==='recovered_by_manual_validation')return 'Recovered';
103
  if(s.includes('success')||s.includes('succeed')||s.includes('passed')||s==='done'||s==='completed')return 'Success';
104
  if(s.includes('running')||s.includes('queued')||s.includes('pending')||s.includes('building')||s.includes('waiting'))return 'Running';
105
  if(s.includes('manual')||s.includes('hardware'))return 'Manual';
 
168
  const manual=buildManualValidationStatus(run);
169
  const manualText=manual.passed?'Manual validation passed':manual.failed?'Manual validation attempted':'';
170
  const linkedText=manual.linkedCount?`${manual.linkedCount} linked test${manual.linkedCount===1?'':'s'}`:'';
171
+ const policy=runSpaceTestPolicy(run);
172
+ const policyText=policy.mode==='complete'?'Can complete with Space Test':policy.mode==='recover'?'Can recover with Space Test':policy.mode==='blocked'?policy.label:'';
173
+ const parts=[manualText,linkedText,policyText,run.target_space,mode?'Goal: '+implementationModeLabel(mode,{short:true}):'',run.selected_hardware].filter(Boolean);
174
  return parts.join(' • ');
175
  }
176
  function runCardMeta(run={}){return runSecondaryLine(run)}
177
  function renderRunRows(runs,compact=false){
178
+ return validRunsList(runs||[]).map(r=>{
179
  const validation=isValidationRunSummary(r);
180
  const kind=runCardKind(r);
181
  // const status=runCanonicalStatus(r)||r.status||'unknown'
 
229
  root.innerHTML=`<button class="secondary small" data-page-action="prev" ${state.runPage<=0?'disabled':''}>‹</button><span>Page ${state.runPage+1} / ${pages}</span><button class="secondary small" data-page-action="next" ${state.runPage>=pages-1?'disabled':''}>›</button>`;
230
  }
231
 
232
+ function isValidationRunSummary(run={}){run=safeRunObject(run);return isValidationRunDetail({summary:run,state:run,kind:run.kind,run_type:run.run_type})}
233
+ function isLinkedValidationRun(run={}){return isValidationRunSummary(run)&&Boolean(linkedValidationParentId(run));}
234
+ function isLegacyValidationRun(run={}){return isValidationRunSummary(run)&&!linkedValidationParentId(run);}
235
 
236
  function linkedValidationParentId(run={}){
237
+ run=safeRunObject(run);
238
  return run.parent_build_run_id||run.parentBuildRunId||run.summary?.parent_build_run_id||run.state?.parent_build_run_id||'';
239
  }
240
  function linkedValidationsForBuild(buildRunId){
241
  if(!buildRunId)return [];
242
+ return validRunsList(state.runsCache||[]).filter(r=>isLinkedValidationRun(r)&&linkedValidationParentId(r)===buildRunId);
243
  }
244
+ function linkedValidationDeleteSummary(runId,detail={}){
245
+ const id=runId||detail.run_id||detail.summary?.run_id||'';
246
+ const rows=linkedValidationsForBuild(id);
247
+ return {count:rows.length,ids:rows.map(r=>r.run_id||r.summary?.run_id).filter(Boolean)};
248
+ }
249
+ function runSpaceTestPolicy(run={}){
250
+ run=safeRunObject(run);
251
+ return run.space_test_policy||run.summary?.space_test_policy||run.view?.space_test_policy||run.space_test?.policy||{};
252
+ }
253
+ function runNeedsLinkedValidation(run={}){
254
+ const policy=runSpaceTestPolicy(run);
255
+ return ['complete','recover'].includes(String(policy.mode||'').toLowerCase())&&policy.enabled!==false&&!buildManualValidationStatus(run).passed;
256
+ }
257
+
258
  function buildManualValidationStatus(run={}){
259
+ run=safeRunObject(run);
260
  const explicit=run.manual_validation_status||run.summary?.manual_validation_status||{};
261
  const linked=linkedValidationsForBuild(run.run_id).filter(v=>!runMatchesStatus(v,'running'));
262
  const passed=Boolean(run.manual_validation_passed||run.summary?.manual_validation_passed||explicit.status==='success'||linked.some(v=>runMatchesStatus(v,'full_inference_success')));
 
265
  return {passed,failed,linkedCount:linked.length,latest,explicit};
266
  }
267
  function buildEffectiveStatus(run={}){
268
+ run=safeRunObject(run);
269
  const manual=buildManualValidationStatus(run);
270
  const eff=String(run.effective_status||run.summary?.effective_status||manual.explicit.effective_status||'').toLowerCase();
271
  if(eff)return eff;
272
+ // v190.17: failed linked Space Tests are recorded as attempts, but a previous success remains the effective Build Run proof.
273
  if(manual.passed)return 'validated_after_manual_space_test';
274
  return runCanonicalStatus(run);
275
  }
276
  function explorerPrimaryRuns(){
277
+ return validRunsList(state.runsCache||[]).filter(r=>!isValidationRunSummary(r));
278
  }
279
+ function runStatTime(run={}){run=safeRunObject(run);const raw=run.updated_at||run.created_at||run.started_at||run.cancelled_at||'';return raw?formatTime(raw):'—'}
280
  function runStatLatency(run={}){
281
+ run=safeRunObject(run);
282
  const latency=run.latency_seconds||run.observed_latency_seconds||run.generation_smoke?.latency_seconds||run.generation_smoke?.observed_latency_seconds;
283
  return latency?fmtSeconds(Number(latency)):'';
284
  }
285
  function runStatLatencyValue(run={}){
286
+ run=safeRunObject(run);
287
  const latency=run.latency_seconds||run.observed_latency_seconds||run.generation_smoke?.latency_seconds||run.generation_smoke?.observed_latency_seconds;
288
  const n=Number(latency);
289
  return Number.isFinite(n)&&n>0?n:null;
 
298
  }
299
  function pct(part,total){return total?`${Math.round((part/total)*100)}%`:'—'}
300
  function runStatsIssueCounts(runs=[]){
301
+ runs=validRunsList(runs);
302
  const counts={failed_builds:0,failed_linked_tests:0,manual_hardware:0,pi_model_mismatch:0,stale:0};
303
  for(const r of runs){
304
  const validation=isValidationRunSummary(r);
305
  const status=String(r.status||'').toLowerCase();
306
  const pi=r.pi_model_resolution||{};
307
+ if(runMatchesStatus(r,'failed')){if(validation&&isLinkedValidationRun(r))counts.failed_linked_tests++;else if(!validation)counts.failed_builds++;}
308
  if(!validation&&runMatchesStatus(r,'manual_hardware_required')) counts.manual_hardware++;
309
  if(!validation&&(pi.mismatch||status.includes('model_mismatch'))) counts.pi_model_mismatch++;
310
  if(status.includes('stale')) counts.stale++;
 
317
  function renderRunAggregateStats(){
318
  const root=$('runStatsList');
319
  if(!root)return;
320
+ const runs=validRunsList(state.runsCache||[]);
321
  const builds=runs.filter(r=>!isValidationRunSummary(r));
322
+ const validations=runs.filter(r=>isLinkedValidationRun(r));
323
  const runningBuilds=builds.filter(r=>runMatchesStatus(r,'running')).length;
324
  const failedBuilds=builds.filter(r=>runMatchesStatus(r,'failed')).length;
325
  const terminalBuilds=builds.filter(r=>!runMatchesStatus(r,'running'));
326
  const automaticSuccess=terminalBuilds.filter(r=>runMatchesStatus(r,'full_inference_success')).length;
327
  const successBuilds=automaticSuccess;
328
  const effectiveSuccess=terminalBuilds.filter(r=>runMatchesStatus({...r,status:buildEffectiveStatus(r)},'full_inference_success')).length;
329
+ const recoveredBuilds=terminalBuilds.filter(r=>String(buildEffectiveStatus(r)).toLowerCase()==='recovered_by_manual_validation').length;
330
+ const needsValidation=terminalBuilds.filter(r=>runNeedsLinkedValidation(r)).length;
331
  // Legacy aggregate labels kept for regression tests: Total runs / Test response time / Typical / slowest recent / Build success / Validation pass.
332
  // Legacy formula kept for audit readability: pct(successBuilds,terminalBuilds.length) and pct(passedValidations,terminalValidations.length).
333
  const passedLinked=validations.filter(r=>!runMatchesStatus(r,'running')&&runMatchesStatus(r,'full_inference_success')).length;
 
360
  ${runStatsCell('Build status',`${runningBuilds} running`,`${failedBuilds} failed` ,failedBuilds?'warn':runningBuilds?'running':'success')}
361
  ${runStatsCell('Automatic success',pct(automaticSuccess,terminalBuilds.length),`${automaticSuccess}/${terminalBuilds.length} finished builds`,'success')}
362
  ${runStatsCell('Effective success',pct(effectiveSuccess,terminalBuilds.length),`${effectiveSuccess}/${terminalBuilds.length} after linked tests`,'success')}
363
+ ${runStatsCell('Recovered',String(recoveredBuilds),`${needsValidation} need validation/recovery`,recoveredBuilds?'success':needsValidation?'warn':'neutral')}
364
  </div>
365
  <div class="run-stats-band linked-tests-band">
366
  <span>Linked Space Tests</span>
 
394
  }
395
  if(state.documentCache)delete state.documentCache[runId];
396
  if(state.latencyCache)delete state.latencyCache[runId];
397
+ // Legacy invariant: state.runsCache=(state.runsCache||[]).filter(r=>r.run_id!==runId)
398
+ state.runsCache=validRunsList(state.runsCache||[]).filter(r=>(r.run_id||r.summary?.run_id)!==runId);
399
  document.querySelectorAll(`.run-row[data-run="${CSS.escape(runId)}"]`).forEach(row=>row.remove());
400
  }
401
  function activeRunIds(){
402
+ return new Set(validRunsList(state.runsCache||[]).map(r=>r.run_id||r.summary?.run_id).filter(Boolean));
403
  }
404
 
405
  function associatedSpaceFromDetail(detail={},summaryOverride={}){
 
446
  });
447
  }
448
  function renderRunsFromCache(){
449
+ state.runsCache=sortRunsNewestFirst(validRunsList(state.runsCache||[]));
450
  const runs=filteredRuns();
451
  const perPage=state.runsPerPage||5;
452
  state.runPage=Math.min(Math.max(0,state.runPage||0),Math.max(0,Math.ceil(runs.length/perPage)-1));
 
468
 
469
  function mergeRunExplorerTerminalLocks(runs=[]){
470
  if(typeof state==='undefined'||!state.validationTerminalByRun)return runs||[];
471
+ return validRunsList(runs).map(run=>{
472
  const validation=isValidationRunSummary(run);
473
  const runId=run.run_id||run.summary?.run_id||'';
474
  const terminal=validation&&runId?state.validationTerminalByRun[runId]:null;
 
476
  return {...run,status:terminal.canonical,validation_status:terminal.canonical,result_status:terminal.canonical,summary:{...(run.summary||{}),status:terminal.canonical,validation_status:terminal.canonical,result_status:terminal.canonical}};
477
  });
478
  }
479
+ async function loadRuns(forceNetwork=false){const stale=Date.now()-(state.runsLastLoaded||0)>45000;const shouldFetch=forceNetwork||!state.runsCache.length||stale;if(!shouldFetch){renderRunsFromCache();return state.runsCache||[]}if(state.runsLoadPromise&&!forceNetwork)return state.runsLoadPromise;document.querySelectorAll('.runs-panel,.full-runs-panel').forEach(x=>x.classList.add('is-refreshing'));state.runsLoadPromise=apiGet(`/api/runs?limit=100&bucket_name=${encodeURIComponent(state.bucketName)}`).then(data=>{// Legacy invariant: .filter(r=>!isRunLocallyDeleted(r.run_id))
480
+ state.runsCache=sortRunsNewestFirst(mergeRunExplorerTerminalLocks(validRunsList(data.runs||[]).filter(r=>!isRunLocallyDeleted(r.run_id||r.summary?.run_id))));state.runsLastLoaded=Date.now();if(data.bucket_source)state.bucketSource=data.bucket_source;renderRunsFromCache();return state.runsCache}).catch(e=>{for(const id of ['runsTable','runsTableHome']){const root=$(id);if(root)root.textContent='Could not load runs: '+e.message}if(forceNetwork)console.warn(e);return state.runsCache||[]}).finally(()=>{state.runsLoadPromise=null;document.querySelectorAll('.runs-panel,.full-runs-panel').forEach(x=>x.classList.remove('is-refreshing'))});return state.runsLoadPromise}
481
  function cachedRunById(runId){
482
  if(isRunLocallyDeleted(runId))return null;
483
+ return validRunsList(state.runsCache||[]).find(r=>(r.run_id||r.summary?.run_id)===runId)||null;
484
  }
485
  function optimisticDetailFromSummary(summary={}){
486
  if(!summary)return null;
 
544
  effective_status:p.summary?.effective_status||p.manual_validation_status?.effective_status,
545
  bucket_source:p.bucket_source||p.summary?.bucket_source||state.bucketSource,
546
  };
547
+ state.runsCache=validRunsList(state.runsCache||[]);
548
+ const idx=state.runsCache.findIndex(r=>(r.run_id||r.summary?.run_id)===runId);
549
  if(idx>=0)state.runsCache[idx]={...state.runsCache[idx],...summary};
550
  else state.runsCache.unshift(summary);
551
  const parentId=summary.parent_build_run_id||linkedValidationParentId(summary);
552
  if(parentId&&isValidationRunSummary(summary)){
553
+ const parentIdx=state.runsCache.findIndex(r=>(r.run_id||r.summary?.run_id)===parentId);
554
  if(parentIdx>=0){
555
  const passed=runMatchesStatus(summary,'full_inference_success');
556
  state.runsCache[parentIdx]={...state.runsCache[parentIdx],manual_validation_passed:state.runsCache[parentIdx].manual_validation_passed||passed,effective_status:passed?'validated_after_manual_space_test':state.runsCache[parentIdx].effective_status,updated_at:summary.updated_at||state.runsCache[parentIdx].updated_at};
 
771
  const label=kind==='validation'?'validation run':'build run';
772
  const associatedSpace=associatedSpaceFromDetail(detail,detail.summary||{});
773
  const canDeleteSpace=runCanOfferSpaceDeletion(kind,detail,detail.summary||{});
774
+ const linkedDelete=kind==='validation'?{count:0,ids:[]}:linkedValidationDeleteSummary(runId,detail);
775
  kicker.textContent=kind==='validation'?'Space Test deletion':'Build run deletion';
776
  title.textContent=`Delete this ${label}?`;
777
  body.textContent=kind==='validation'
778
+ ? 'This removes only this validation run’s linked Space Test artifacts. The parent Build Run and generated Space will not be deleted. The tested Space will not be deleted.'
779
+ : (linkedDelete.count?`This removes this Build Run’s bucket artifacts and ${linkedDelete.count} linked Space Test validation run${linkedDelete.count===1?'':'s'}. The generated Space is kept unless you explicitly choose to delete it below.`:'This removes this Build Run’s bucket artifacts, logs, state, traces and report. The generated Space is kept unless you explicitly choose to delete it below.');
780
+ meta.innerHTML=`<span><strong>Run</strong><code>${escapeHtml(runId||'—')}</code></span><span><strong>Type</strong>${escapeHtml(kind==='validation'?'Linked Space Test':'Build')}</span><span><strong>Status</strong>${escapeHtml(badgeLabel(normalized))}</span>${associatedSpace?`<span><strong>Space</strong><code>${escapeHtml(associatedSpace)}</code></span>`:''}${linkedDelete.count?`<span><strong>Linked tests</strong>${linkedDelete.count}</span>`:''}`;
781
  if(spaceOption&&spaceCheckbox&&spaceLabel){
782
  spaceCheckbox.checked=false;
783
  spaceOption.hidden=!canDeleteSpace;
 
807
  const setDeleting=()=>{
808
  modal.classList.add('is-deleting');
809
  title.textContent=kind==='validation'?'Deleting this validation run…':'Deleting this run…';
810
+ body.textContent=kind==='validation'?'Removing linked validation artifacts and refreshing the parent Build Run.':'Removing bucket artifacts and linked validation runs. The panel will reset as soon as deletion is confirmed.';
811
  confirmBtn.textContent='Deleting…';
812
  confirmBtn.disabled=true;
813
  cancelEls.forEach(el=>{el.disabled=true;el.setAttribute('aria-disabled','true')});
 
818
  submitted=false;
819
  title.textContent=`Delete this ${label}?`;
820
  body.textContent=kind==='validation'
821
+ ? 'This removes only this validation run’s linked Space Test artifacts. The parent Build Run and generated Space will not be deleted. The tested Space will not be deleted.'
822
+ : (linkedDelete.count?`This removes this Build Run’s bucket artifacts and ${linkedDelete.count} linked Space Test validation run${linkedDelete.count===1?'':'s'}. The generated Space is kept unless you explicitly choose to delete it below.`:'This removes this Build Run’s bucket artifacts, logs, state, traces and report. The generated Space is kept unless you explicitly choose to delete it below.');
823
  confirmBtn.textContent=active?'Delete stored files':'Delete run';
824
  confirmBtn.disabled=false;
825
  cancelEls.forEach(el=>{el.disabled=false;el.removeAttribute('aria-disabled')});
 
854
  let success=false;
855
  try{
856
  const result=await apiDelete(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}`,deleteSpace?{delete_space:true}:{});
857
+ const deletedChildren=Array.isArray(result.deleted_linked_validations)?result.deleted_linked_validations:[];
858
+ const childSuffix=deletedChildren.length?` and ${deletedChildren.length} linked validation${deletedChildren.length===1?'':'s'}`:'';
859
  const suffix=deleteSpace?(result.space_deleted?' and associated Space':(result.space_delete_error?' but Space deletion failed':' and Space was already missing')):'';
860
+ showMessage(`Deleted run ${runId}${childSuffix}${suffix}`,'success');
861
  success=true;
862
  rememberDeletedRun(runId);
863
+ deletedChildren.forEach(id=>rememberDeletedRun(id));
864
  clearDeletedRunState(runId,context.kind);
865
  forgetRunEverywhere(runId);
866
+ deletedChildren.forEach(id=>{clearDeletedRunState(id,'validation');forgetRunEverywhere(id);});
867
  renderRunsFromCache();
868
  if(confirmed&&typeof confirmed.close==='function')confirmed.close(true);
869
  loadRuns(true).catch(e=>console.warn('post-delete runs refresh failed',e));
 
1235
  const path=status.archive_relative_path||status.path||'';
1236
  const localLabel=(status.local_record_found||Object.keys(local||{}).length)?'written':'pending';
1237
  const backendLabel=published?'published':attempted&&reason==='record_not_ready'?'pending':attempted?'not published':'pending';
1238
+ const manual=detail.manual_validation_status||detail.summary?.manual_validation_status||detail.view?.manual_validation||{};
1239
+ const effective=manual.status==='success'?'validated_after_manual_space_test':(detail.summary?.effective_status||detail.effective_status||'');
1240
  const intro=published?'Eval record copied to the operator archive bucket.':attempted&&reason==='record_not_ready'?'Eval record will be archived when the run completes.':'Archive publication is not confirmed yet.';
1241
+ const effectiveNote=effective?`<p class="eval-effective-note">Effective verdict: <strong>${escapeHtml(runCompactStatusLabel(effective))}</strong>${manual.validation_run_id?` from linked Space Test <code>${escapeHtml(manual.validation_run_id)}</code>`:''}.</p>`:'';
1242
+ body.innerHTML=`<p>${escapeHtml(intro)}</p>${effectiveNote}<div class="eval-archive-grid"><div><span>Local record</span><strong>${escapeHtml(localLabel)}</strong></div><div><span>Backend copy</span><strong>${escapeHtml(backendLabel)}</strong></div><div><span>Mode</span><strong>${escapeHtml(status.publish_mode||status.mode||'backend')}</strong></div><div><span>Archive bucket</span><strong>${escapeHtml(bucket||'—')}</strong></div></div>${path?`<code>${escapeHtml(path)}</code>`:''}${reason&&!published&&reason!=='record_not_ready'?`<p class="eval-warning">${escapeHtml(reason)}</p>`:''}`;
1243
  }
1244
  function renderRunDetail(detail){
1245
  state.selectedRunDetail=detail;
1246
  const summary=detail.summary||{};
1247
+ if(typeof contextualizeSpaceTestFromBuildRun==='function')contextualizeSpaceTestFromBuildRun(detail);
1248
  const smoke=detail.generation_smoke||{};
1249
  const files=detail.files||[];
1250
  const view=detail.view||{};
 
1295
  // Legacy clean-delete invariant: await loadRuns(true) was intentionally replaced by post-delete background refresh in v190.9.
1296
 
1297
  // Legacy validation preview invariant: if(isValidationRunDetail(detail)){showValidationRunDetail(detail)
1298
+
1299
+ // v190.15 build-first compatibility invariant: legacy/incomplete Build Runs stay visible, validation runs require parent_build_run_id before they can affect parent stats.
web/static/styles.css ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ /* v190.19 eval archive owner controls */
3
+ .eval-archive-actions .eval-flush-button{
4
+ margin-left:auto;
5
+ }
6
+ .eval-archive-panel.manageable .eval-archive-actions{
7
+ gap:10px;
8
+ align-items:center;
9
+ flex-wrap:wrap;
10
+ }
11
+ .eval-effective-note{
12
+ margin:.45rem 0 .55rem;
13
+ line-height:1.35;
14
+ }