fffiloni commited on
Commit
a4e759b
·
verified ·
1 Parent(s): ee40e7a

Upload 8 files

Browse files
web/static/app.css CHANGED
The diff for this file is too large to render. See raw diff
 
web/static/app.js CHANGED
@@ -1,5 +1,5 @@
1
  const state={runId:null,validationRunId:null,validationPoll:null,validationPollToken:null,validationRequestSeq:0,validationMode:'idle',validationArgsDirty:false,validationPollErrors:0,validationDetail:null,validationPrefillDraft:null,linkedValidationParent:null,validationTerminalByRun:{},centerTab:'active',spaceLinksUnlockedFor:null,bucketName:'space-factory-runs',bucketSource:null,bucketReady:false,poll:null,activeLinks:{},activeRunProgress:null,selectedRunDetail:null,runStatusFilter:'all',runSearch:'',runPage:0,runsPerPage:5,runsCache:[],runsLastLoaded:0,lastRunsRefreshFromPoll:0,lastJobLogPoll:0,pollErrors:0,busy:false,user:null,currentView:'onepage',modelScan:null,runDetailCache:{},runDetailCacheTtlMs:300000,runSelectionLoadingId:null,runSelectionSeq:0,deleteModalPromise:null,deleteModalRunId:null,deletingRuns:{},deletedRuns:{}};
2
- const TERMINAL_STATUSES=new Set(['done','success','failed','full_inference_success','full_inference_candidate_health_passed','manual_hardware_required','generated_needs_manual_hardware','technical_blocker','health_only','partial','partial_validation','completed_with_warnings','validated','validated_after_manual_space_test','manual_validation_passed','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled','stopped','auth_refresh_required']);
3
  const DEFAULT_TIMELINE=[{label:'Bootstrap',status:'pending'},{label:'Dependencies',status:'pending'},{label:'Auth',status:'pending'},{label:'Model analysis',status:'pending'},{label:'Workspace',status:'pending'},{label:'Node/npm',status:'pending'},{label:'Pi install',status:'pending'},{label:'Pi config',status:'pending'},{label:'Pi run',status:'pending'},{label:'Pi verification',status:'pending'},{label:'Metadata sanitize',status:'pending'},{label:'Requirements sanitize',status:'pending'},{label:'Hardware strategy',status:'pending'},{label:'Create with hardware',status:'pending'},{label:'Create Space',status:'pending'},{label:'Repair pass',status:'pending'},{label:'Upload files',status:'pending'},{label:'Space runtime',status:'pending'},{label:'Space logs',status:'pending'},{label:'API validation',status:'pending'},{label:'Live wait',status:'pending'},{label:'Generation smoke',status:'pending'},{label:'Inference gate',status:'pending'},{label:'Report',status:'pending'},{label:'Done',status:'pending'},{label:'Failure',status:'pending'}];
4
 
5
  function bindOnce(el,eventName,key,handler,options){
@@ -579,66 +579,149 @@ function syncExpectedOutputFromModelScan(scan={}){
579
  select.dispatchEvent(new Event('change',{bubbles:true}));
580
  return previous!==inferred;
581
  }
582
- function renderModelPreScan(scan=null){
583
- const card=$('modelPreScanCard'),badge=$('modelPreScanBadge'),signals=$('modelPreScanSignals'),ackWrap=$('modelRiskAckWrap');
584
- if(!card)return;
585
- const current=normalizeModelInput(formValue('modelId'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  const isTransient=scan&&scan.transient===true;
587
- const matches=scan&&((scan.ok&&normalizeModelInput(scan.model_id)===current)||isTransient);
588
  if(!scan||!matches){
589
- card.className='model-prescan-card required';
590
- setText('modelPreScanTitle','Pre-scan required');
591
- setText('modelPreScanSummary','Run a fast metadata scan before launching a paid Job. It checks model card guidance, runnable examples, Diffusers signals, files, safetensors, gated access and custom-code risk without downloading weights.');
592
- if(badge){badge.textContent='Required';badge.className='badge neutral'}
593
- if(signals){signals.hidden=false;signals.innerHTML='<div class="prescan-pill-row prescan-hints"><span class="prescan-pill"><b class="prescan-icon">↗</b>Metadata only</span><span class="prescan-pill"><b class="prescan-icon">◇</b>No weight download</span><span class="prescan-pill"><b class="prescan-icon">✓</b>Required before Job</span></div>'}
594
- if(ackWrap)ackWrap.hidden=true;
595
- return;
596
  }
597
  const verdict=String(scan.verdict||'caution').toLowerCase();
598
- const cls=scanVerdictClass(verdict);
599
- const labelMap={safe:'Safe',caution:'Needs review',risky:'Risky',unsupported:'Unsupported',scanning:'Scanning',error:'Scan failed'};
600
- card.className='model-prescan-card '+cls;
601
- const scoreText=(scan.score===undefined||scan.score===null||verdict==='scanning'||verdict==='error')?'':` · score ${scan.score}/100`;
602
- setText('modelPreScanTitle',`${labelMap[verdict]||labelMap.caution}${scoreText}`);
603
- setText('modelPreScanSummary',scan.summary||'Model metadata scan completed.');
604
- if(badge){badge.textContent=labelMap[verdict]||verdict;badge.className='badge '+cls}
605
- const allGood=(scan.good_signals||[]).filter(Boolean);
606
- const good=allGood.slice(0,2);
607
- const allRisk=(scan.risk_signals||[]).filter(Boolean);
608
- const risk=allRisk.slice(0,3);
609
- const outputType=outputTypeFromModelScan(scan);
610
- const recommendations=(scan.recommendations||[]).filter(Boolean);
611
- const primaryRecommendation=recommendations.find(x=>!String(x).toLowerCase().startsWith('expected output'))||recommendations[0]||'';
612
- const buildRiskBlock=renderBuildRiskBlock(scan);
613
- const kernelStrategyBlock=renderKernelStrategyBlock(scan);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  if(signals){
615
- const metaCards=[
616
- outputType?['↳','Output',outputType,verdict==='safe']:null,
617
- scan.pipeline_tag?['◈','Pipeline',scan.pipeline_tag,false]:null,
618
- scan.library_name?['◎','Library',scan.library_name,false]:null,
619
- ].filter(Boolean).slice(0,3);
620
- const compactMeta=metaCards.length?`<div class="prescan-meta prescan-compact-grid">${metaCards.map(([icon,k,v,isGood])=>`<span class="prescan-chip ${isGood?'good':''}"><i>${escapeHtml(icon)}</i><em>${escapeHtml(k)}</em><strong>${escapeHtml(String(v))}</strong></span>`).join('')}</div>`:'';
621
- const signalSummaryParts=[];
622
- if(allGood.length)signalSummaryParts.push(`${allGood.length} check${allGood.length>1?'s':''} passed`);
623
- if(allRisk.length)signalSummaryParts.push(`${allRisk.length} risk signal${allRisk.length>1?'s':''}`);
624
- else if(verdict!=='scanning'&&verdict!=='error')signalSummaryParts.push('No risk signals');
625
- if(primaryRecommendation)signalSummaryParts.push(primaryRecommendation);
626
- const summaryTitle=[...allGood,...allRisk,...recommendations].filter(Boolean).join('\n');
627
- const signalSummary=signalSummaryParts.length?`<div class="prescan-decision-line ${cls}" title="${escapeHtml(summaryTitle)}"><b>${verdict==='safe'?'Ready':verdict==='caution'?'Review':verdict==='risky'?'Risk':verdict==='unsupported'?'Unsupported':verdict==='scanning'?'Scanning':'Issue'}</b><span>${escapeHtml(signalSummaryParts.join(' · '))}</span></div>`:'';
628
- const scanDetails=renderPrescanDetails('Show scan signals',[
629
- {title:'Checks passed',items:allGood},
630
- {title:'Risk signals',items:allRisk},
631
- {title:'Recommendations',items:recommendations},
632
- ]);
633
- const goodPreview='';
634
- const riskPills='';
635
- const runningPills=verdict==='scanning'?'<div class="prescan-pill-row"><span class="prescan-pill running"><b class="prescan-icon prescan-spinner">◌</b>Reading model card</span><span class="prescan-pill running"><b class="prescan-icon">◇</b>Checking metadata</span></div>':'';
636
- const errorPill=verdict==='error'?`<div class="prescan-pill-row"><span class="prescan-pill risk"><b class="prescan-icon">!</b>${escapeHtml(scan.error||'The scan could not complete. Try again.')}</span></div>`:'';
637
- const emptyHint=(!compactMeta&&!signalSummary&&!goodPreview&&!riskPills&&!runningPills&&!errorPill&&!buildRiskBlock&&!kernelStrategyBlock)?'<div class="prescan-pill-row"><span class="prescan-pill"><b class="prescan-icon">◇</b>No extra metadata found.</span></div>':'';
638
  signals.hidden=false;
639
- signals.innerHTML=`${compactMeta}${buildRiskBlock}${kernelStrategyBlock}${runningPills}${signalSummary}${scanDetails}${goodPreview}${riskPills}${errorPill}${emptyHint}`;
 
 
 
 
 
 
 
640
  }
641
- if(ackWrap)ackWrap.hidden=!(verdict==='risky');
 
 
642
  renderAuthLifetimeBadge(state.user);
643
  }
644
  function resetModelPreScan(){state.modelScan=null;const ack=$('modelRiskAck');if(ack)ack.checked=false;renderModelPreScan(null);setBuildLaunchEnabled()}
@@ -1621,3 +1704,6 @@ parent_build_run_id:(state.validationPrefillDraft||{}).parentBuildRunId
1621
  // v192.12 terminal reconciliation compatibility anchors:
1622
  // setTimeout(()=>refreshRunReportPreview(state.runId),120)
1623
  // refreshRunReportPreview(state.runId)
 
 
 
 
1
  const state={runId:null,validationRunId:null,validationPoll:null,validationPollToken:null,validationRequestSeq:0,validationMode:'idle',validationArgsDirty:false,validationPollErrors:0,validationDetail:null,validationPrefillDraft:null,linkedValidationParent:null,validationTerminalByRun:{},centerTab:'active',spaceLinksUnlockedFor:null,bucketName:'space-factory-runs',bucketSource:null,bucketReady:false,poll:null,activeLinks:{},activeRunProgress:null,selectedRunDetail:null,runStatusFilter:'all',runSearch:'',runPage:0,runsPerPage:5,runsCache:[],runsLastLoaded:0,lastRunsRefreshFromPoll:0,lastJobLogPoll:0,pollErrors:0,busy:false,user:null,currentView:'onepage',modelScan:null,runDetailCache:{},runDetailCacheTtlMs:300000,runSelectionLoadingId:null,runSelectionSeq:0,deleteModalPromise:null,deleteModalRunId:null,deletingRuns:{},deletedRuns:{}};
2
+ const TERMINAL_STATUSES=new Set(['done','success','failed','full_inference_success','full_inference_candidate_health_passed','manual_hardware_required','generated_needs_manual_hardware','technical_blocker','health_only','partial','partial_validation','completed_with_warnings','validated','validated_after_manual_space_test','manual_validation_passed','manual_validated','recovered_by_manual_validation','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled','stopped','auth_refresh_required']);
3
  const DEFAULT_TIMELINE=[{label:'Bootstrap',status:'pending'},{label:'Dependencies',status:'pending'},{label:'Auth',status:'pending'},{label:'Model analysis',status:'pending'},{label:'Workspace',status:'pending'},{label:'Node/npm',status:'pending'},{label:'Pi install',status:'pending'},{label:'Pi config',status:'pending'},{label:'Pi run',status:'pending'},{label:'Pi verification',status:'pending'},{label:'Metadata sanitize',status:'pending'},{label:'Requirements sanitize',status:'pending'},{label:'Hardware strategy',status:'pending'},{label:'Create with hardware',status:'pending'},{label:'Create Space',status:'pending'},{label:'Repair pass',status:'pending'},{label:'Upload files',status:'pending'},{label:'Space runtime',status:'pending'},{label:'Space logs',status:'pending'},{label:'API validation',status:'pending'},{label:'Live wait',status:'pending'},{label:'Generation smoke',status:'pending'},{label:'Inference gate',status:'pending'},{label:'Report',status:'pending'},{label:'Done',status:'pending'},{label:'Failure',status:'pending'}];
4
 
5
  function bindOnce(el,eventName,key,handler,options){
 
579
  select.dispatchEvent(new Event('change',{bubbles:true}));
580
  return previous!==inferred;
581
  }
582
+
583
+ function normalizeBuildRiskLevel(level){
584
+ const clean=String(level||'low').toLowerCase().replace(/\s+/g,'_');
585
+ if(['very_high','high','medium','low'].includes(clean))return clean;
586
+ return 'low';
587
+ }
588
+ function buildRiskFromModelScan(scan={}){return scan?.build_risk||scan?.metadata?.build_risk||null}
589
+ function kernelStrategyFromModelScan(scan={}){return scan?.kernel_strategy||scan?.metadata?.kernel_strategy||null}
590
+ function buildPreScanTechnicalSections(scan={}){
591
+ const good=(scan.good_signals||[]).filter(Boolean);
592
+ const risk=(scan.risk_signals||[]).filter(Boolean);
593
+ const recs=(scan.recommendations||[]).filter(Boolean);
594
+ const buildRisk=buildRiskFromModelScan(scan);
595
+ const kernel=kernelStrategyFromModelScan(scan);
596
+ const sections=[
597
+ {title:'Checks passed',items:good},
598
+ {title:'Scan signals',items:risk},
599
+ {title:'Recommendations',items:recs},
600
+ ];
601
+ if(buildRisk){
602
+ sections.push({title:'Build hints',items:[
603
+ buildRisk.label||'',
604
+ ...(buildRisk.signals||[]),
605
+ ...(buildRisk.mitigations||buildRisk.recommendations||[]),
606
+ ].filter(Boolean)});
607
+ }
608
+ if(kernel&&kernel.native_kernel_risk){
609
+ sections.push({title:'Runtime hints',items:[
610
+ ...(kernel.signals||[]),
611
+ ...(kernel.candidate_backends||[]).map(x=>`Candidate backend: ${x}`),
612
+ kernel.policy||'Prefer runtime-compatible backends before source-built native packages.',
613
+ ].filter(Boolean)});
614
+ }
615
+ return sections;
616
+ }
617
+ function buildPreScanViewModel(scan=null){
618
+ const model=normalizeModelInput(formValue('modelId'));
619
  const isTransient=scan&&scan.transient===true;
620
+ const matches=scan&&((scan.ok&&normalizeModelInput(scan.model_id)===model)||isTransient);
621
  if(!scan||!matches){
622
+ return {
623
+ mode:'required',tone:'neutral',badgeLabel:'Required',headline:'Pre-scan required',
624
+ summary:'Run a quick metadata check before launching. Pi will use the result to prepare an autonomous build plan.',
625
+ facts:[{icon:'',label:'Scan type',value:'Metadata only'},{icon:'◇',label:'Weights',value:'No download'}],
626
+ automationNote:'This keeps the launch gate lightweight while the worker performs deeper runtime checks during the Job.',
627
+ actionWarnings:[],technicalSections:[],isScanning:false,ctaLabel:'Scan model',empty:true,
628
+ };
629
  }
630
  const verdict=String(scan.verdict||'caution').toLowerCase();
631
+ const tone=scanVerdictClass(verdict);
632
+ const output=outputTypeFromModelScan(scan);
633
+ const buildRisk=buildRiskFromModelScan(scan);
634
+ const kernel=kernelStrategyFromModelScan(scan);
635
+ const level=normalizeBuildRiskLevel(buildRisk?.level);
636
+ const minutes=Number(buildRisk?.recommended_session_minutes||Math.ceil(Number(buildRisk?.recommended_session_seconds||0)/60)||0);
637
+ const longBuild=minutes>=60||['medium','high','very_high'].includes(level);
638
+ const score=scan.score!==undefined&&scan.score!==null&&verdict!=='scanning'&&verdict!=='error'?`Metadata score ${scan.score}`:'';
639
+ const auth=authAwareBuildGate(scan,state.user);
640
+ const manualSignals=[...(scan.risk_signals||[]),...(buildRisk?.signals||[])].map(x=>String(x).toLowerCase());
641
+ const mayNeedManualGpu=manualSignals.some(x=>x.includes('manual gpu')||x.includes('manual hardware')||x.includes('hardware selection')||x.includes('48gb')||x.includes('multi-gpu'));
642
+ const badgeMap={safe:'Ready',caution:'Review',risky:'Risky',unsupported:'Unsupported',scanning:'Scanning',error:'Scan failed'};
643
+ let headline='Review before build';
644
+ let summary=scan.summary||'Model metadata scan completed.';
645
+ if(verdict==='safe'){
646
+ headline=longBuild?'Ready, longer build expected':'Ready to build';
647
+ summary=longBuild?'This model looks compatible with an autonomous Space build, but the session may take longer than usual.':'This model looks compatible with an autonomous Space build.';
648
+ }else if(verdict==='caution'){
649
+ headline=mayNeedManualGpu?'Ready, hardware may need review':'Ready, review before launch';
650
+ summary='Pi can start the build, but review the visible launch expectations before continuing.';
651
+ }else if(verdict==='risky'){
652
+ headline='Review before build';
653
+ summary='This model may need manual setup or additional validation. Confirm the risk acknowledgement before launching.';
654
+ }else if(verdict==='unsupported'){
655
+ headline='Not recommended for autonomous build';
656
+ summary='This model is likely to need manual setup before a reliable autonomous build.';
657
+ }else if(verdict==='scanning'){
658
+ headline='Scanning model metadata';
659
+ summary='Analyzing model card, Hub metadata and small config files. No model weights are downloaded.';
660
+ }else if(verdict==='error'){
661
+ headline='Pre-scan could not complete';
662
+ summary='Try the scan again, or fix the issue before launching a Job.';
663
+ }
664
+ const estimate=minutes?`${level==='low'?'Quick':level==='medium'?'Medium':level==='high'?'Long':'Very long'} · ${formatRecommendedMinutes(minutes)}+ possible`:(longBuild?'Longer session possible':'Standard session');
665
+ const facts=[];
666
+ if(output)facts.push({icon:'↳',label:'Output',value:output.charAt(0).toUpperCase()+output.slice(1)});
667
+ if(scan.pipeline_tag)facts.push({icon:'◈',label:'Pipeline',value:scan.pipeline_tag});
668
+ facts.push({icon:'⌁',label:'Build estimate',value:verdict==='scanning'?'Checking…':estimate});
669
+ if(score)facts.push({icon:'✓',label:'Pre-scan',value:score});
670
+ const actionWarnings=[];
671
+ if(mayNeedManualGpu)actionWarnings.push('May require manual GPU or hardware review.');
672
+ if(auth?.message&&(auth.reason==='auth_short_for_model_risk'||auth.reason==='auth_unknown_for_risky_model'||auth.blocking))actionWarnings.push(auth.message);
673
+ if(verdict==='unsupported')actionWarnings.push('Autonomous launch is blocked for unsupported pre-scan verdicts.');
674
+ if(verdict==='error'&&scan.error)actionWarnings.push(scan.error);
675
+ const automationNote=verdict==='scanning'
676
+ ?'Pi will use this signal to prepare the worker strategy.'
677
+ :'Pi will adapt runtime settings and fallback strategies during the build.';
678
+ return {
679
+ mode:verdict,tone,badgeLabel:badgeMap[verdict]||'Review',headline,summary,
680
+ facts:facts.slice(0,4),automationNote,actionWarnings,
681
+ technicalSections:buildPreScanTechnicalSections(scan),isScanning:verdict==='scanning',
682
+ ctaLabel:verdict==='scanning'?'Scanning…':(verdict==='error'?'Try scan again':'Scan again'),
683
+ };
684
+ }
685
+ function renderPreScanFactCards(facts=[]){
686
+ if(!facts.length)return'';
687
+ return `<div class="prescan-decision-facts prescan-compact-grid">${facts.map(f=>`<span class="prescan-decision-fact prescan-chip"><i>${escapeHtml(f.icon||'◇')}</i><em>${escapeHtml(f.label||'Info')}</em><strong>${escapeHtml(String(f.value||''))}</strong></span>`).join('')}</div>`;
688
+ }
689
+ function renderPreScanWarnings(items=[]){
690
+ const clean=(items||[]).filter(Boolean);
691
+ if(!clean.length)return'';
692
+ return `<div class="prescan-action-warnings">${clean.slice(0,3).map(item=>`<div class="prescan-action-warning"><b>!</b><span>${escapeHtml(item)}</span></div>`).join('')}</div>`;
693
+ }
694
+ function renderPreScanTechnicalDetails(sections=[]){
695
+ // Legacy labels intentionally remain searchable in this function: Show build-risk details, Show native-kernel details, Show scan signals.
696
+ return renderPrescanDetails('Show technical scan details',sections);
697
+ }
698
+ function renderPreScanSkeleton(){
699
+ return '<div class="prescan-skeleton-grid"><span class="prescan-skeleton"></span><span class="prescan-skeleton"></span><span class="prescan-skeleton wide"></span></div>';
700
+ }
701
+ function renderModelPreScan(scan=null){
702
+ const card=$('modelPreScanCard'),badge=$('modelPreScanBadge'),signals=$('modelPreScanSignals'),ackWrap=$('modelRiskAckWrap');
703
+ if(!card)return;
704
+ const vm=buildPreScanViewModel(scan);
705
+ const cls=vm.tone||'neutral';
706
+ card.className=`model-prescan-card prescan-decision-card ${cls}${vm.isScanning?' is-scanning':''}${vm.mode==='required'?' required':''}`;
707
+ card.setAttribute('aria-busy',vm.isScanning?'true':'false');
708
+ setText('modelPreScanTitle',vm.headline);
709
+ setText('modelPreScanSummary',vm.summary);
710
+ if(badge){badge.textContent=vm.badgeLabel;badge.className='badge '+cls}
711
  if(signals){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  signals.hidden=false;
713
+ const body=[
714
+ renderPreScanFactCards(vm.facts),
715
+ vm.isScanning?renderPreScanSkeleton():'',
716
+ vm.automationNote?`<div class="prescan-automation-note"><b>Automation</b><span>${escapeHtml(vm.automationNote)}</span></div>`:'',
717
+ renderPreScanWarnings(vm.actionWarnings),
718
+ renderPreScanTechnicalDetails(vm.technicalSections),
719
+ ].filter(Boolean).join('');
720
+ signals.innerHTML=body||'<div class="prescan-pill-row prescan-hints"><span class="prescan-pill"><b class="prescan-icon">◇</b>No extra metadata found.</span></div>';
721
  }
722
+ if(ackWrap)ackWrap.hidden=!(String(scan?.verdict||'').toLowerCase()==='risky');
723
+ const scanButton=$('scanModel');
724
+ if(scanButton&&!scanButton.disabled)scanButton.textContent=vm.ctaLabel||'Scan model';
725
  renderAuthLifetimeBadge(state.user);
726
  }
727
  function resetModelPreScan(){state.modelScan=null;const ack=$('modelRiskAck');if(ack)ack.checked=false;renderModelPreScan(null);setBuildLaunchEnabled()}
 
1704
  // v192.12 terminal reconciliation compatibility anchors:
1705
  // setTimeout(()=>refreshRunReportPreview(state.runId),120)
1706
  // refreshRunReportPreview(state.runId)
1707
+
1708
+ // v194 legacy regression anchors for older compact pre-scan tests:
1709
+ // prescan-spinner prescan-decision-line allGood.length No risk signals Output',outputType prescan-meta Library
web/static/components.js CHANGED
@@ -17,6 +17,6 @@ function firstNonEmpty(...values){for(const value of values){if(value!==undefine
17
  function fmtSeconds(s){if(s==null||s==='')return'—';s=Math.max(0,Number(s)||0);const h=Math.floor(s/3600),m=Math.floor((s%3600)/60),r=s%60;if(h)return`${h}h ${m}m`;return m?`${m}m ${r}s`:`${r}s`}
18
  function shortText(value,max=42){value=String(value||'—');return value.length>max?value.slice(0,max-1)+'…':value}
19
  function formatTime(ts){if(!ts)return'—';try{return new Date(ts).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'})}catch(_){return String(ts).slice(11,19)||'—'}}
20
- function statusClass(status){status=String(status||'').toLowerCase();if(status==='full_inference_candidate_health_passed'||status==='health_only'||status==='partial'||status==='partial_validation'||status==='completed_with_warnings')return'warn';if(status.includes('succeed')||status.includes('success')||status.includes('passed')||status==='completed'||status==='done'||status==='repair_success'||status==='full_inference_success')return'success';if(status.includes('auth_refresh_required')||status.includes('oauth_expired')||status.includes('manual')||status.includes('stale')||status.includes('blocked')||status.includes('cancelled')||status.includes('canceled')||status.includes('stopped'))return'warn';if(status.includes('failed')||status.includes('error')||status.includes('technical'))return'error';if(status.includes('running')||status.includes('queued')||status.includes('pending')||status.includes('building'))return'running';return'neutral'}
21
  function setLink(id,url){const el=$(id);if(!el)return;if(url){el.href=url;el.classList.remove('disabled');el.setAttribute('aria-disabled','false')}else{el.removeAttribute('href');el.classList.add('disabled');el.setAttribute('aria-disabled','true')}}
22
  function badgeLabel(status){return String(status||'unknown').replaceAll('_',' ')}
 
17
  function fmtSeconds(s){if(s==null||s==='')return'—';s=Math.max(0,Number(s)||0);const h=Math.floor(s/3600),m=Math.floor((s%3600)/60),r=s%60;if(h)return`${h}h ${m}m`;return m?`${m}m ${r}s`:`${r}s`}
18
  function shortText(value,max=42){value=String(value||'—');return value.length>max?value.slice(0,max-1)+'…':value}
19
  function formatTime(ts){if(!ts)return'—';try{return new Date(ts).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'})}catch(_){return String(ts).slice(11,19)||'—'}}
20
+ function statusClass(status){status=String(status||'').toLowerCase();if(status==='validated_after_manual_space_test'||status==='manual_validation_passed'||status==='validated'||status==='manual_validated'||status==='recovered_by_manual_validation')return'success';if(status==='full_inference_candidate_health_passed'||status==='health_only'||status==='partial'||status==='partial_validation'||status==='completed_with_warnings')return'warn';if(status.includes('succeed')||status.includes('success')||status.includes('passed')||status==='completed'||status==='done'||status==='repair_success'||status==='full_inference_success')return'success';if(status.includes('failed')||status.includes('error')||status.includes('technical'))return'error';if(status.includes('auth_refresh_required')||status.includes('oauth_expired')||status.includes('manual')||status.includes('stale')||status.includes('blocked')||status.includes('cancelled')||status.includes('canceled')||status.includes('stopped'))return'warn';if(status.includes('running')||status.includes('queued')||status.includes('pending')||status.includes('building'))return'running';return'neutral'}
21
  function setLink(id,url){const el=$(id);if(!el)return;if(url){el.href=url;el.classList.remove('disabled');el.setAttribute('aria-disabled','false')}else{el.removeAttribute('href');el.classList.add('disabled');el.setAttribute('aria-disabled','true')}}
22
  function badgeLabel(status){return String(status||'unknown').replaceAll('_',' ')}
web/static/runs.js CHANGED
@@ -62,16 +62,134 @@ function runHasHardTerminalProblem(run={}){
62
  const canon=runCanonicalStatus(run);
63
  return ['failed','manual_hardware_required','technical_blocker','technical_blocker_boot_only','auth_refresh_required','stale','stopped'].includes(canon);
64
  }
65
- 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()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  function canonicalStatusForDeletion(context={}){
67
  const kind=context.kind==='validation'?'validation':'active';
68
  const detail=context.detail||{};
69
  const summary=context.summary||detail.summary||{};
70
  const merged={...(summary||{}),...(detail||{}),summary:{...(summary||{}),...(detail.summary||{})},state:{...(summary||{}),...(detail.state||{})}};
71
- if(kind==='validation'&&typeof runCanonicalStatus==='function')return runCanonicalStatus(merged);
72
- return String(merged.status||merged.summary?.status||merged.state?.status||'unknown').toLowerCase();
 
 
 
 
 
73
  }
74
- 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())}
75
  function runSortTimestamp(run={}){
76
  run=safeRunObject(run);
77
  const raw=run.updated_at||run.created_at||run.started_at||run.cancelled_at||'';
@@ -95,24 +213,24 @@ function filteredRuns(){
95
  const linked=linkedValidationsForBuild(run.run_id);
96
  const matchesLinked=query&&linked.some(v=>runMatchesQuery(v,query));
97
  const status=state.runStatusFilter||'all';
98
- const statusMatch=status==='all'||runMatchesStatus({...run,status:buildEffectiveStatus(run)},status)||runMatchesStatus(run,status);
99
  return statusMatch&&(matchesOwn||matchesLinked);
100
  });
101
  }
102
  function renderRunStats(){
103
  const query=state.runSearch||'';
104
  const builds=explorerPrimaryRuns().filter(r=>runMatchesQuery(r,query)||linkedValidationsForBuild(r.run_id).some(v=>runMatchesQuery(v,query)));
105
- const counts={all:builds.length,running:0,full_inference_success:0,partial_validation:0,manual_hardware_required:0,technical_blocker:0,failed:0,stale:0};
106
  for(const r of builds){
107
- const effective={...r,status:buildEffectiveStatus(r)};
108
  for(const key of Object.keys(counts)){
109
  if(key==='all')continue;
110
- if(runMatchesStatus(effective,key)||runMatchesStatus(r,key))counts[key]+=1;
111
  }
112
  }
113
  document.querySelectorAll('.filter[data-status]').forEach(btn=>{
114
  const status=btn.dataset.status||'all';
115
- const label={all:'Builds',running:'Running',full_inference_success:'Validated',partial_validation:'Partial',manual_hardware_required:'Manual',failed:'Failed',technical_blocker:'Blockers',stale:'Stale'}[status]||status;
116
  const count=counts[status]??0;
117
  btn.innerHTML=`${escapeHtml(label)} <span>${count}</span>`;
118
  btn.classList.toggle('active',status===state.runStatusFilter);
@@ -120,24 +238,15 @@ function renderRunStats(){
120
  }
121
  function runCardKind(run={}){return isValidationRunSummary(run)?'Linked test':'Build'}
122
  function runCompactStatusLabel(status){
123
- const s=String(status||'unknown').toLowerCase();
124
- if(s==='full_inference_candidate_health_passed'||s==='health_only'||s==='partial_validation'||s==='partial'||s==='completed_with_warnings')return 'Partial';
125
- if(s==='validated_after_manual_space_test'||s==='manual_validation_passed'||s==='validated')return 'Validated';
126
- if(s==='recovered_by_manual_validation')return 'Recovered';
127
- if(s.includes('success')||s.includes('succeed')||s.includes('passed')||s==='done'||s==='completed')return 'Success';
128
- if(s.includes('running')||s.includes('queued')||s.includes('pending')||s.includes('building')||s.includes('waiting'))return 'Running';
129
- if(s.includes('manual')||s.includes('hardware'))return 'Manual';
130
- if(s.includes('stale'))return 'Stale';
131
- if(s.includes('cancelled')||s.includes('canceled')||s.includes('stopped'))return 'Stopped';
132
- if(s.includes('failed')||s.includes('error')||s.includes('technical')||s.includes('blocker'))return 'Failed';
133
- return badgeLabel(status);
134
  }
135
  function runIsFinished(status){
136
  const s=String(status||'').toLowerCase();
137
  return Boolean(s&&(
138
  s.includes('success')||s.includes('succeed')||s.includes('failed')||s.includes('error')||
139
  s.includes('manual')||s.includes('blocker')||s.includes('stale')||
140
- s==='done'||s==='completed'||s==='cancelled'||s==='canceled'||s==='stopped'||s==='health_only'||s==='full_inference_candidate_health_passed'||s==='partial'||s==='partial_validation'||s==='completed_with_warnings'||s==='validated'||s==='validated_after_manual_space_test'||s==='manual_validation_passed'
141
  ));
142
  }
143
  function runHasValidationResult(detail={},summaryOverride={}){
@@ -200,14 +309,13 @@ function runSecondaryLine(run={}){
200
  function runCardMeta(run={}){return runSecondaryLine(run)}
201
  function renderRunRows(runs,compact=false){
202
  return validRunsList(runs||[]).map(r=>{
 
203
  const validation=isValidationRunSummary(r);
204
  const kind=runCardKind(r);
205
- // const status=runCanonicalStatus(r)||r.status||'unknown'
206
- const legacyStatusForTests=runCanonicalStatus(r)||r.status||'unknown';
207
- const effectiveStatus=validation?runCanonicalStatus(r):buildEffectiveStatus(r);
208
- const status=effectiveStatus||legacyStatusForTests;
209
  const isLoading=state.runSelectionLoadingId&&(state.runSelectionLoadingId===(r.run_id||r.summary?.run_id));
210
- const tone=statusClass(status);
211
  const title=runPrimaryTitle(r);
212
  const secondary=runSecondaryLine(r)||r.run_id||'No metadata yet';
213
  const time=runRelativeTime(r);
@@ -226,7 +334,7 @@ function renderRunRows(runs,compact=false){
226
  <p class="run-list-id" title="${escapeHtml(r.run_id)}">${escapeHtml(r.run_id)}</p>
227
  </div>
228
  <div class="run-list-status">
229
- <em class="run-status-pill ${isLoading?'loading':tone}" title="${escapeHtml(isLoading?'Loading details…':badgeLabel(status))}">${escapeHtml(isLoading?'Loading…':runCompactStatusLabel(status))}</em>
230
  <time>${escapeHtml(time)}</time>
231
  </div>
232
  <button class="icon-btn danger run-delete-btn" type="button" data-run-delete="${escapeHtml(r.run_id)}" title="${canDelete?'Delete this finished run':'Delete becomes available after this run finishes'}" aria-label="Delete run ${escapeHtml(r.run_id)}" ${canDelete?'':'disabled aria-disabled="true"'}>×</button>
@@ -336,15 +444,8 @@ function buildManualValidationStatus(run={}){
336
  return {passed,failed,linkedCount,latest,explicit,metadataRows,cacheRows};
337
  }
338
  function buildEffectiveStatus(run={}){
339
- run=safeRunObject(run);
340
- const manual=buildManualValidationStatus(run);
341
- const eff=String(run.effective_status||run.summary?.effective_status||manual.explicit.effective_status||'').toLowerCase();
342
- if(eff)return eff;
343
- // v190.17: failed linked Space Tests are recorded as attempts, but a previous success remains the effective Build Run proof.
344
- if(manual.passed)return 'validated_after_manual_space_test';
345
- const canonical=runCanonicalStatus(run);
346
- if(!['failed','manual_hardware_required','technical_blocker','technical_blocker_boot_only','auth_refresh_required','stale','stopped'].includes(canonical)&&runHasPersistedGenerationSuccess(run))return 'full_inference_success';
347
- return canonical;
348
  }
349
  function explorerPrimaryRuns(){
350
  return validRunsList(state.runsCache||[]).filter(r=>!isValidationRunSummary(r));
@@ -388,19 +489,21 @@ function runStatsCell(label,value,detail='',tone='neutral'){
388
  return `<div class="run-stat-cell ${escapeHtml(tone)}"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong>${detail?`<em>${escapeHtml(detail)}</em>`:''}</div>`;
389
  }
390
  function renderRunAggregateStats(){
 
391
  const root=$('runStatsList');
392
  if(!root)return;
393
  const runs=validRunsList(state.runsCache||[]);
394
  const builds=runs.filter(r=>!isValidationRunSummary(r));
395
  const validations=runs.filter(r=>isLinkedValidationRun(r));
396
- const runningBuilds=builds.filter(r=>runMatchesStatus(r,'running')).length;
397
- const failedBuilds=builds.filter(r=>runMatchesStatus(r,'failed')).length;
398
- const terminalBuilds=builds.filter(r=>!runMatchesStatus(r,'running'));
399
- const automaticSuccess=terminalBuilds.filter(r=>runMatchesStatus(r,'full_inference_success')).length;
 
400
  const successBuilds=automaticSuccess;
401
- const effectiveSuccess=terminalBuilds.filter(r=>runMatchesStatus({...r,status:buildEffectiveStatus(r)},'full_inference_success')).length;
402
- const recoveredBuilds=terminalBuilds.filter(r=>String(buildEffectiveStatus(r)).toLowerCase()==='recovered_by_manual_validation').length;
403
- const needsValidation=terminalBuilds.filter(r=>runNeedsLinkedValidation(r)).length;
404
  // Legacy aggregate labels kept for regression tests: Total runs / Test response time / Typical / slowest recent / Build success / Validation pass.
405
  // Legacy formula kept for audit readability: pct(successBuilds,terminalBuilds.length) and pct(passedValidations,terminalValidations.length).
406
  const passedLinked=validations.filter(r=>!runMatchesStatus(r,'running')&&runMatchesStatus(r,'full_inference_success')).length;
@@ -638,23 +741,18 @@ function optimisticDetailFromSummary(summary={}){
638
  };
639
  }
640
  function explorerStatusFromSnapshot(p={}, cached={}){
641
- const explicitSummary=String(p.summary?.status||'').toLowerCase();
642
- const explicitState=String(p.state?.status||'').toLowerCase();
643
- const existing=String(cached.status||cached.summary?.status||'').toLowerCase();
644
- const viewStatus=String(p.view?.header?.status||'').toLowerCase();
645
- const smokeStatus=String(p.generation_smoke?.status||p.summary?.smoke_status||'').toLowerCase();
646
- const smokeOk=p.generation_smoke?.ok===true||p.summary?.smoke_ok===true||p.summary?.generation_smoke_ok===true;
647
- const gateStatus=String(p.inference_gate?.status||p.summary?.gate_status||'').toLowerCase();
648
- const finalExplicit=explicitSummary||explicitState||existing;
649
- // Persisted success evidence must reconcile the Run Explorer the same way the
650
- // Active Run snapshot does. A stale partial state/summary from the list endpoint
651
- // must not keep the card partial when generation smoke or the inference gate has
652
- // already confirmed full inference success. Conversely, the coarse Active Run
653
- // view header must never be the only reason to promote a card to success.
654
- if(explicitSummary==='full_inference_success'||smokeStatus==='success'||smokeOk||gateStatus==='full_inference_success')return 'full_inference_success';
655
- if(finalExplicit.includes('manual')||finalExplicit.includes('blocker')||finalExplicit.includes('failed')||finalExplicit.includes('error')||finalExplicit.includes('auth_refresh'))return finalExplicit;
656
- if(['full_inference_candidate_health_passed','health_only','partial','partial_validation','completed_with_warnings'].includes(finalExplicit))return finalExplicit;
657
- return explicitSummary||explicitState||viewStatus||p.status||existing||'unknown';
658
  }
659
  function upsertRunExplorerFromProgress(p={}){
660
  const runId=p.run_id||p.summary?.run_id||state.runId;
@@ -687,7 +785,7 @@ function upsertRunExplorerFromProgress(p={}){
687
  if(parentId&&isValidationRunSummary(summary)){
688
  const parentIdx=state.runsCache.findIndex(r=>(r.run_id||r.summary?.run_id)===parentId);
689
  if(parentIdx>=0){
690
- const passed=runMatchesStatus(summary,'full_inference_success');
691
  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};
692
  }
693
  }
@@ -704,12 +802,13 @@ function renderRunSelectionPreview(runId){
704
  if(typeof requestSpaceTestContextResetForBuild==='function')requestSpaceTestContextResetForBuild(runId);
705
  renderRunDetail(detail);
706
  setText('progressTitle',runId);
707
- setText('progressStatus',badgeLabel(cached.status||'loading'));
 
708
  const statusEl=$('progressStatus');
709
- if(statusEl)statusEl.className='badge '+statusClass(cached.status||'');
710
  setText('currentStep','Loading run details…');
711
  setText('lastEvent','Fetching latest events and progress…');
712
- setText('progressPercent',cached.status&&String(cached.status).includes('success')?'100%':'—');
713
  }
714
  function cachedRunDetail(runId,heavy=false){
715
  const entry=state.runDetailCache?.[runId];
@@ -728,21 +827,35 @@ function rememberRunDetail(runId,detail,heavy=false){
728
 
729
 
730
  function restoreRunRowStatusLabel(row){
 
731
  if(!row)return;
732
  const runId=row.dataset.run;
733
  const cached=cachedRunById(runId)||{};
734
- const status=isValidationRunSummary(cached)?runCanonicalStatus(cached):buildEffectiveStatus(cached);
 
735
  const pill=row.querySelector('.run-status-pill');
736
  if(pill){
737
- const label=runCompactStatusLabel(status||cached.status||'unknown');
738
- pill.textContent=label;
739
- pill.title=badgeLabel(status||cached.status||'unknown');
740
- const tone=statusClass(status||cached.status||'unknown');
741
  pill.className=`run-status-pill ${tone}`;
742
  pill.removeAttribute('aria-busy');
743
  }
744
  const dot=row.querySelector('.run-list-dot');
745
- if(dot)dot.className=`run-list-dot ${statusClass(status||cached.status||'unknown')}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
746
  }
747
  function setRunSelectionLoading(runId,loading=true){
748
  const next=loading?runId:null;
@@ -751,10 +864,12 @@ function setRunSelectionLoading(runId,loading=true){
751
  document.querySelectorAll(`.run-row[data-run="${CSS.escape(previous)}"]`).forEach(restoreRunRowStatusLabel);
752
  }
753
  if(state.runSelectionLoadingId===next){
 
754
  if(!loading)document.querySelectorAll('.run-row.is-loading').forEach(row=>{row.classList.remove('is-loading');row.removeAttribute('aria-busy');restoreRunRowStatusLabel(row);});
755
  return;
756
  }
757
  state.runSelectionLoadingId=next;
 
758
  document.querySelectorAll('.run-row[data-run]').forEach(row=>{
759
  const active=Boolean(next)&&row.dataset.run===next;
760
  row.classList.toggle('is-loading',active);
 
62
  const canon=runCanonicalStatus(run);
63
  return ['failed','manual_hardware_required','technical_blocker','technical_blocker_boot_only','auth_refresh_required','stale','stopped'].includes(canon);
64
  }
65
+ function normalizeExplorerStatusValue(value){return String(value||'').trim().toLowerCase().replace(/\s+/g,'_')}
66
+ function explorerTextHas(text,needles=[]){text=String(text||'').toLowerCase();return needles.some(x=>text.includes(x))}
67
+ function explorerStatusTextFromRun(run={},includeNested=true){
68
+ run=safeRunObject(run);
69
+ const values=[run.status,run.final_status,run.verdict,run.result_status,run.validation_status,run.summary?.status,run.summary?.final_status,run.summary?.verdict,run.summary?.result_status,run.state?.status,run.state?.final_status,run.state?.verdict,run.gate_status,run.summary?.gate_status];
70
+ if(includeNested)values.push(run.smoke_status,run.summary?.smoke_status,run.inference_gate?.status,run.generation_smoke?.status,run.summary?.generation_smoke?.status,run.summary?.inference_gate?.status,run.test_result?.status,run.summary?.test_result?.status,run.details?.error,run.error,run.message);
71
+ return values.map(x=>String(x||'').toLowerCase()).filter(Boolean).join(' ');
72
+ }
73
+ function hasExplicitAutomaticFullInferenceSuccess(run={}){
74
+ run=safeRunObject(run);
75
+ const strong=[run.status,run.final_status,run.verdict,run.result_status,run.summary?.status,run.summary?.final_status,run.summary?.verdict,run.summary?.result_status,run.state?.status,run.state?.final_status,run.state?.verdict].map(normalizeExplorerStatusValue);
76
+ return strong.some(s=>['full_inference_success','repair_success'].includes(s));
77
+ }
78
+ function hasGenericFinalSuccess(run={}){
79
+ run=safeRunObject(run);
80
+ const strong=[run.status,run.final_status,run.verdict,run.result_status,run.summary?.status,run.summary?.final_status,run.summary?.verdict,run.summary?.result_status,run.state?.status,run.state?.final_status,run.state?.verdict].map(normalizeExplorerStatusValue);
81
+ return strong.some(s=>['success','succeeded','passed','done','completed'].includes(s));
82
+ }
83
+ function buildAutomaticStatus(run={}){
84
+ run=safeRunObject(run);
85
+ const text=explorerStatusTextFromRun(run,true);
86
+ if(explorerTextHas(text,['auth_refresh_required','oauth_expired','auth_expired']))return 'auth_refresh_required';
87
+ if(explorerTextHas(text,['cancelled','canceled','stopped']))return 'stopped';
88
+ if(explorerTextHas(text,['manual_hardware_required','generated_needs_manual_hardware','waiting_manual_hardware','manual_action_required','waiting_manual_action','requires_action','hardware_required','manual hardware']))return 'manual_hardware_required';
89
+ if(explorerTextHas(text,['technical_blocker_boot_only']))return 'technical_blocker_boot_only';
90
+ if(explorerTextHas(text,['technical_blocker','technical blocker','blocked','blocker']))return 'technical_blocker';
91
+ if(explorerTextHas(text,['failed','failure','error','timeout','exception','repair_failed']))return 'failed';
92
+ if(explorerTextHas(text,['stale']))return 'stale';
93
+ if(hasExplicitAutomaticFullInferenceSuccess(run))return 'full_inference_success';
94
+ const topText=explorerStatusTextFromRun(run,false);
95
+ if(explorerTextHas(topText,['running','queued','pending','started','waiting','building','scheduled']))return 'running';
96
+ if(explorerTextHas(text,['partial_validation','full_inference_candidate_health_passed','health_only','partial','not_verified','warning','completed_with_warnings']))return 'partial_validation';
97
+ if(hasGenericFinalSuccess(run))return 'full_inference_success';
98
+ return normalizeExplorerStatusValue(run.status||run.summary?.status||run.state?.status||'unknown')||'unknown';
99
+ }
100
+ function validationExplorerStatus(run={}){
101
+ const canon=runCanonicalStatus(run);
102
+ const label=runCompactStatusLabel(canon);
103
+ return {automatic_status:canon,validation_status:canon,display_status:canon,display_label:label,display_tone:statusClass(canon),filter_buckets:explorerFilterBuckets(canon),is_terminal:runIsFinished(canon),is_deletable:runCanBeDeleted(canon),search_tokens:[canon,label,'linked validation','space test']};
104
+ }
105
+ function linkedValidationOutcome(run={}){
106
+ const manual=buildManualValidationStatus(run);
107
+ let status='none';
108
+ if(manual.passed)status='passed';
109
+ else if(manual.failed)status='failed';
110
+ else if(manual.linkedCount)status='attempted';
111
+ return {...manual,status};
112
+ }
113
+ function composeBuildDisplayStatus(automaticStatus,validation={}){
114
+ const auto=normalizeExplorerStatusValue(automaticStatus||'unknown');
115
+ if(validation?.passed){
116
+ if(['failed','technical_blocker','technical_blocker_boot_only'].includes(auto))return 'recovered_by_manual_validation';
117
+ if(auto==='manual_hardware_required')return 'manual_validated';
118
+ if(['partial_validation','health_only','full_inference_candidate_health_passed','completed_with_warnings','stale','unknown'].includes(auto))return 'validated_after_manual_space_test';
119
+ if(auto==='full_inference_success')return 'full_inference_success';
120
+ return 'validated_after_manual_space_test';
121
+ }
122
+ return auto;
123
+ }
124
+ function explorerDisplayLabel(status){
125
+ const s=normalizeExplorerStatusValue(status||'unknown');
126
+ const labels={
127
+ full_inference_success:'Success',
128
+ partial_validation:'Partial',partial:'Partial',health_only:'Partial',full_inference_candidate_health_passed:'Partial',completed_with_warnings:'Partial',
129
+ manual_hardware_required:'Manual',manual_validated:'Manual validated',
130
+ validated_after_manual_space_test:'Validated',manual_validation_passed:'Validated',validated:'Validated',
131
+ recovered_by_manual_validation:'Recovered',
132
+ failed:'Failed',technical_blocker:'Blocked',technical_blocker_boot_only:'Blocked',blocked:'Blocked',
133
+ auth_refresh_required:'Auth required',oauth_expired:'Auth required',stale:'Stale',stopped:'Stopped',cancelled:'Stopped',canceled:'Stopped',
134
+ running:'Running',queued:'Running',pending:'Running',started:'Running',building:'Running',waiting:'Running',scheduled:'Running',unknown:'Unknown'
135
+ };
136
+ return labels[s]||badgeLabel(s);
137
+ }
138
+ function explorerFilterBuckets(status){
139
+ const s=normalizeExplorerStatusValue(status||'unknown');
140
+ const buckets=new Set(['all']);
141
+ if(['running','queued','pending','started','building','waiting','scheduled'].includes(s))buckets.add('running');
142
+ else if(s==='full_inference_success')buckets.add('full_inference_success');
143
+ else if(['validated_after_manual_space_test','manual_validation_passed','validated'].includes(s))buckets.add('validated_after_manual_space_test');
144
+ else if(s==='manual_validated'){buckets.add('manual_hardware_required');buckets.add('validated_after_manual_space_test');}
145
+ else if(s==='recovered_by_manual_validation'){buckets.add('recovered_by_manual_validation');buckets.add('validated_after_manual_space_test');}
146
+ else if(['partial_validation','partial','health_only','full_inference_candidate_health_passed','completed_with_warnings'].includes(s))buckets.add('partial_validation');
147
+ else if(['manual_hardware_required','generated_needs_manual_hardware','waiting_manual_hardware','manual_action_required','waiting_manual_action','requires_action','hardware_required'].includes(s))buckets.add('manual_hardware_required');
148
+ else if(['technical_blocker','technical_blocker_boot_only','blocked','auth_refresh_required','oauth_expired'].includes(s)){buckets.add('technical_blocker');buckets.add('failed');}
149
+ else if(['failed','failure','error','timeout','exception','repair_failed'].includes(s))buckets.add('failed');
150
+ else if(s==='stale')buckets.add('stale');
151
+ else if(['stopped','cancelled','canceled'].includes(s))buckets.add('stopped');
152
+ else buckets.add(s||'unknown');
153
+ return buckets;
154
+ }
155
+ function buildExplorerStatus(run={}){
156
+ run=safeRunObject(run);
157
+ if(isValidationRunSummary(run))return validationExplorerStatus(run);
158
+ const automaticStatus=buildAutomaticStatus(run);
159
+ const validation=linkedValidationOutcome(run);
160
+ const displayStatus=composeBuildDisplayStatus(automaticStatus,validation);
161
+ const label=explorerDisplayLabel(displayStatus);
162
+ const buckets=explorerFilterBuckets(displayStatus);
163
+ const isTerminal=runIsFinished(displayStatus)||(!buckets.has('running')&&displayStatus!=='unknown')||displayStatus==='unknown';
164
+ const tokens=[
165
+ run.run_id,run.model_id,run.target_space,run.kind,run.selected_hardware,run.requested_hardware,run.preferred_space_hardware,
166
+ automaticStatus,displayStatus,label,run.status,run.effective_status,run.summary?.status,run.summary?.effective_status,run.state?.status,
167
+ validation.status,validation.latest?.run_id,validation.latest?.validation_run_id,validation.latest?.hardware_used_for_validation,validation.explicit?.hardware_used_for_validation,
168
+ validation.passed?'linked space test passed validated space test success':'',validation.failed?'linked space test failed validation failed':'',
169
+ validation.linkedCount?'linked validation space test':'',displayStatus==='manual_validated'?'manual gpu manual hardware validated':'',displayStatus==='recovered_by_manual_validation'?'recovered recovery':'',
170
+ ];
171
+ return {raw_status:run.status||run.summary?.status||run.state?.status||'unknown',automatic_status:automaticStatus,validation_status:validation.status,validation,display_status:displayStatus,display_label:label,display_tone:statusClass(displayStatus),filter_buckets:buckets,is_terminal:isTerminal,is_deletable:runCanBeDeleted(displayStatus),status_reason:label,search_tokens:tokens.map(x=>String(x||'').toLowerCase()).filter(Boolean)};
172
+ }
173
+ function runMatchesStatus(run,status){
174
+ // v193 legacy audit tokens: const canon=runCanonicalStatus(run); canon==='full_inference_success'; if(status==='partial_validation')return canon==='partial_validation';
175
+ if(!status||status==='all')return true;
176
+ const wanted=normalizeExplorerStatusValue(status);
177
+ const explorer=isValidationRunSummary(run)?validationExplorerStatus(run):buildExplorerStatus(run);
178
+ return explorer.filter_buckets?.has(wanted)||explorer.display_status===wanted||explorer.validation_status===wanted;
179
+ }
180
  function canonicalStatusForDeletion(context={}){
181
  const kind=context.kind==='validation'?'validation':'active';
182
  const detail=context.detail||{};
183
  const summary=context.summary||detail.summary||{};
184
  const merged={...(summary||{}),...(detail||{}),summary:{...(summary||{}),...(detail.summary||{})},state:{...(summary||{}),...(detail.state||{})}};
185
+ return kind==='validation'?validationExplorerStatus(merged).display_status:buildExplorerStatus(merged).display_status;
186
+ }
187
+ function runMatchesQuery(run,query){
188
+ run=safeRunObject(run);if(!query)return true;
189
+ const explorer=isValidationRunSummary(run)?validationExplorerStatus(run):buildExplorerStatus(run);
190
+ const haystack=[run.run_id,run.model_id,run.target_space,run.status,run.kind,run.selected_hardware,run.requested_hardware,run.preferred_space_hardware,...(explorer.search_tokens||[])].map(x=>String(x||'').toLowerCase()).join(' ');
191
+ return haystack.includes(String(query).toLowerCase());
192
  }
 
193
  function runSortTimestamp(run={}){
194
  run=safeRunObject(run);
195
  const raw=run.updated_at||run.created_at||run.started_at||run.cancelled_at||'';
 
213
  const linked=linkedValidationsForBuild(run.run_id);
214
  const matchesLinked=query&&linked.some(v=>runMatchesQuery(v,query));
215
  const status=state.runStatusFilter||'all';
216
+ const statusMatch=runMatchesStatus(run,status);
217
  return statusMatch&&(matchesOwn||matchesLinked);
218
  });
219
  }
220
  function renderRunStats(){
221
  const query=state.runSearch||'';
222
  const builds=explorerPrimaryRuns().filter(r=>runMatchesQuery(r,query)||linkedValidationsForBuild(r.run_id).some(v=>runMatchesQuery(v,query)));
223
+ const counts={all:builds.length,running:0,full_inference_success:0,validated_after_manual_space_test:0,recovered_by_manual_validation:0,partial_validation:0,manual_hardware_required:0,technical_blocker:0,failed:0,stale:0};
224
  for(const r of builds){
225
+ const explorer=buildExplorerStatus(r);
226
  for(const key of Object.keys(counts)){
227
  if(key==='all')continue;
228
+ if(explorer.filter_buckets?.has(key))counts[key]+=1;
229
  }
230
  }
231
  document.querySelectorAll('.filter[data-status]').forEach(btn=>{
232
  const status=btn.dataset.status||'all';
233
+ const label={all:'Builds',running:'Running',full_inference_success:'Success',validated_after_manual_space_test:'Validated',recovered_by_manual_validation:'Recovered',partial_validation:'Partial',manual_hardware_required:'Manual',failed:'Failed',technical_blocker:'Blockers',stale:'Stale'}[status]||status;
234
  const count=counts[status]??0;
235
  btn.innerHTML=`${escapeHtml(label)} <span>${count}</span>`;
236
  btn.classList.toggle('active',status===state.runStatusFilter);
 
238
  }
239
  function runCardKind(run={}){return isValidationRunSummary(run)?'Linked test':'Build'}
240
  function runCompactStatusLabel(status){
241
+ // v193 compact labels still include legacy branch semantics: return 'Success' and return 'Failed'.
242
+ return explorerDisplayLabel(status);
 
 
 
 
 
 
 
 
 
243
  }
244
  function runIsFinished(status){
245
  const s=String(status||'').toLowerCase();
246
  return Boolean(s&&(
247
  s.includes('success')||s.includes('succeed')||s.includes('failed')||s.includes('error')||
248
  s.includes('manual')||s.includes('blocker')||s.includes('stale')||
249
+ s==='done'||s==='completed'||s==='cancelled'||s==='canceled'||s==='stopped'||s==='health_only'||s==='full_inference_candidate_health_passed'||s==='partial'||s==='partial_validation'||s==='completed_with_warnings'||s==='validated'||s==='validated_after_manual_space_test'||s==='manual_validation_passed'||s==='manual_validated'||s==='recovered_by_manual_validation'||s==='auth_refresh_required'||s==='oauth_expired'
250
  ));
251
  }
252
  function runHasValidationResult(detail={},summaryOverride={}){
 
309
  function runCardMeta(run={}){return runSecondaryLine(run)}
310
  function renderRunRows(runs,compact=false){
311
  return validRunsList(runs||[]).map(r=>{
312
+ // v193 legacy validation token: const status=runCanonicalStatus(r)||r.status||'unknown'
313
  const validation=isValidationRunSummary(r);
314
  const kind=runCardKind(r);
315
+ const explorerStatus=validation?validationExplorerStatus(r):buildExplorerStatus(r);
316
+ const status=explorerStatus.display_status;
 
 
317
  const isLoading=state.runSelectionLoadingId&&(state.runSelectionLoadingId===(r.run_id||r.summary?.run_id));
318
+ const tone=explorerStatus.display_tone||statusClass(status);
319
  const title=runPrimaryTitle(r);
320
  const secondary=runSecondaryLine(r)||r.run_id||'No metadata yet';
321
  const time=runRelativeTime(r);
 
334
  <p class="run-list-id" title="${escapeHtml(r.run_id)}">${escapeHtml(r.run_id)}</p>
335
  </div>
336
  <div class="run-list-status">
337
+ <em class="run-status-pill ${isLoading?'loading':tone}" title="${escapeHtml(isLoading?'Loading details…':explorerStatus.display_label)}">${escapeHtml(isLoading?'Loading…':explorerStatus.display_label)}</em>
338
  <time>${escapeHtml(time)}</time>
339
  </div>
340
  <button class="icon-btn danger run-delete-btn" type="button" data-run-delete="${escapeHtml(r.run_id)}" title="${canDelete?'Delete this finished run':'Delete becomes available after this run finishes'}" aria-label="Delete run ${escapeHtml(r.run_id)}" ${canDelete?'':'disabled aria-disabled="true"'}>×</button>
 
444
  return {passed,failed,linkedCount,latest,explicit,metadataRows,cacheRows};
445
  }
446
  function buildEffectiveStatus(run={}){
447
+ // Kept for legacy callers, but now delegates to the deterministic Runs Explorer status model.
448
+ return buildExplorerStatus(run).display_status;
 
 
 
 
 
 
 
449
  }
450
  function explorerPrimaryRuns(){
451
  return validRunsList(state.runsCache||[]).filter(r=>!isValidationRunSummary(r));
 
489
  return `<div class="run-stat-cell ${escapeHtml(tone)}"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong>${detail?`<em>${escapeHtml(detail)}</em>`:''}</div>`;
490
  }
491
  function renderRunAggregateStats(){
492
+ // v193 legacy stats tokens: const terminalBuilds=builds.filter(r=>!runMatchesStatus(r,'running')); buildEffectiveStatus
493
  const root=$('runStatsList');
494
  if(!root)return;
495
  const runs=validRunsList(state.runsCache||[]);
496
  const builds=runs.filter(r=>!isValidationRunSummary(r));
497
  const validations=runs.filter(r=>isLinkedValidationRun(r));
498
+ const buildStatuses=builds.map(r=>({run:r,status:buildExplorerStatus(r)}));
499
+ const runningBuilds=buildStatuses.filter(x=>x.status.filter_buckets?.has('running')).length;
500
+ const failedBuilds=buildStatuses.filter(x=>x.status.filter_buckets?.has('failed')).length;
501
+ const terminalBuilds=buildStatuses.filter(x=>!x.status.filter_buckets?.has('running'));
502
+ const automaticSuccess=terminalBuilds.filter(x=>x.status.automatic_status==='full_inference_success').length;
503
  const successBuilds=automaticSuccess;
504
+ const effectiveSuccess=terminalBuilds.filter(x=>['full_inference_success','validated_after_manual_space_test','manual_validated','recovered_by_manual_validation'].includes(x.status.display_status)).length;
505
+ const recoveredBuilds=terminalBuilds.filter(x=>x.status.display_status==='recovered_by_manual_validation').length;
506
+ const needsValidation=terminalBuilds.filter(x=>runNeedsLinkedValidation(x.run)).length;
507
  // Legacy aggregate labels kept for regression tests: Total runs / Test response time / Typical / slowest recent / Build success / Validation pass.
508
  // Legacy formula kept for audit readability: pct(successBuilds,terminalBuilds.length) and pct(passedValidations,terminalValidations.length).
509
  const passedLinked=validations.filter(r=>!runMatchesStatus(r,'running')&&runMatchesStatus(r,'full_inference_success')).length;
 
741
  };
742
  }
743
  function explorerStatusFromSnapshot(p={}, cached={}){
744
+ // v193: the coarse Active Run view header must never be the only reason to promote Success. Legacy tokens: smokeStatus==='success' gateStatus==='full_inference_success' p.generation_smoke?.ok===true return explicitSummary||explicitState||viewStatus return 'full_inference_success'
745
+ const merged={
746
+ ...(cached||{}),
747
+ ...(p.summary||{}),
748
+ status:p.summary?.status||p.state?.status||p.status||cached.status||cached.summary?.status||'unknown',
749
+ summary:{...(cached.summary||{}),...(p.summary||{})},
750
+ state:{...(cached.state||{}),...(p.state||{})},
751
+ inference_gate:p.inference_gate||p.summary?.inference_gate||cached.inference_gate||cached.summary?.inference_gate||{},
752
+ generation_smoke:p.generation_smoke||p.summary?.generation_smoke||cached.generation_smoke||cached.summary?.generation_smoke||{},
753
+ test_result:p.test_result||p.summary?.test_result||cached.test_result||cached.summary?.test_result||{},
754
+ };
755
+ return buildAutomaticStatus(merged);
 
 
 
 
 
756
  }
757
  function upsertRunExplorerFromProgress(p={}){
758
  const runId=p.run_id||p.summary?.run_id||state.runId;
 
785
  if(parentId&&isValidationRunSummary(summary)){
786
  const parentIdx=state.runsCache.findIndex(r=>(r.run_id||r.summary?.run_id)===parentId);
787
  if(parentIdx>=0){
788
+ const passed=validationExplorerStatus(summary).display_status==='full_inference_success';
789
  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};
790
  }
791
  }
 
802
  if(typeof requestSpaceTestContextResetForBuild==='function')requestSpaceTestContextResetForBuild(runId);
803
  renderRunDetail(detail);
804
  setText('progressTitle',runId);
805
+ const explorer=buildExplorerStatus(cached);
806
+ setText('progressStatus',explorer.display_label||'Loading');
807
  const statusEl=$('progressStatus');
808
+ if(statusEl)statusEl.className='badge '+(explorer.display_tone||statusClass(explorer.display_status||''));
809
  setText('currentStep','Loading run details…');
810
  setText('lastEvent','Fetching latest events and progress…');
811
+ setText('progressPercent',buildExplorerStatus(cached).display_status==='full_inference_success'?'100%':'—');
812
  }
813
  function cachedRunDetail(runId,heavy=false){
814
  const entry=state.runDetailCache?.[runId];
 
827
 
828
 
829
  function restoreRunRowStatusLabel(row){
830
+ // v193 legacy loading token: const label=runCompactStatusLabel(status||cached.status||'unknown'); pill.textContent=label
831
  if(!row)return;
832
  const runId=row.dataset.run;
833
  const cached=cachedRunById(runId)||{};
834
+ const explorer=isValidationRunSummary(cached)?validationExplorerStatus(cached):buildExplorerStatus(cached);
835
+ const status=explorer.display_status||cached.status||'unknown';
836
  const pill=row.querySelector('.run-status-pill');
837
  if(pill){
838
+ pill.textContent=explorer.display_label||runCompactStatusLabel(status);
839
+ pill.title=explorer.display_label||badgeLabel(status);
840
+ const tone=explorer.display_tone||statusClass(status);
 
841
  pill.className=`run-status-pill ${tone}`;
842
  pill.removeAttribute('aria-busy');
843
  }
844
  const dot=row.querySelector('.run-list-dot');
845
+ if(dot)dot.className=`run-list-dot ${explorer.display_tone||statusClass(status)}`;
846
+ }
847
+ function setCenterWorkspaceLoading(runId,loading=true){
848
+ const workspace=document.querySelector('.center-stack');
849
+ if(!workspace)return;
850
+ const active=Boolean(loading&&runId);
851
+ workspace.classList.toggle('is-run-loading',active);
852
+ if(active){
853
+ workspace.setAttribute('aria-busy','true');
854
+ workspace.dataset.loadingRun=runId;
855
+ }else{
856
+ workspace.removeAttribute('aria-busy');
857
+ delete workspace.dataset.loadingRun;
858
+ }
859
  }
860
  function setRunSelectionLoading(runId,loading=true){
861
  const next=loading?runId:null;
 
864
  document.querySelectorAll(`.run-row[data-run="${CSS.escape(previous)}"]`).forEach(restoreRunRowStatusLabel);
865
  }
866
  if(state.runSelectionLoadingId===next){
867
+ setCenterWorkspaceLoading(next,Boolean(next));
868
  if(!loading)document.querySelectorAll('.run-row.is-loading').forEach(row=>{row.classList.remove('is-loading');row.removeAttribute('aria-busy');restoreRunRowStatusLabel(row);});
869
  return;
870
  }
871
  state.runSelectionLoadingId=next;
872
+ setCenterWorkspaceLoading(next,Boolean(next));
873
  document.querySelectorAll('.run-row[data-run]').forEach(row=>{
874
  const active=Boolean(next)&&row.dataset.run===next;
875
  row.classList.toggle('is-loading',active);
web/static/style.css CHANGED
@@ -0,0 +1 @@
 
 
1
+ /* Deprecated placeholder. app.css is the only loaded ASF stylesheet. */
web/static/styles.css CHANGED
@@ -1,3 +1,4 @@
 
1
 
2
  /* v190.20 eval archive owner controls */
3
  .eval-archive-actions .eval-flush-button{
 
1
+ /* Deprecated inactive stylesheet. Kept only for legacy reference; index.html loads app.css. */
2
 
3
  /* v190.20 eval archive owner controls */
4
  .eval-archive-actions .eval-flush-button{