fffiloni commited on
Commit
c2bd1f6
·
verified ·
1 Parent(s): 2ef87b0

Upload 6 files

Browse files
web/static/app.css CHANGED
@@ -638,3 +638,66 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
638
  }
639
  .activity-row.error .event-reason{color:#be123c}
640
  .activity-row.hardware-fallback .event-reason{color:#1d4ed8}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
  }
639
  .activity-row.error .event-reason{color:#be123c}
640
  .activity-row.hardware-fallback .event-reason{color:#1d4ed8}
641
+
642
+ /* v87: validation is a separate Space test workflow */
643
+ .validation-help{
644
+ margin:0 0 12px;
645
+ padding:8px 10px;
646
+ border:1px solid var(--line);
647
+ border-radius:10px;
648
+ background:#f8fafc;
649
+ font-size:11px;
650
+ line-height:1.35;
651
+ }
652
+ .validation-result-card .badge.running{background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe}
653
+ .validation-result-card .badge.success{background:#ecfdf3;color:#027a48;border-color:#bbf7d0}
654
+ .validation-result-card .badge.error{background:#fff1f2;color:#be123c;border-color:#fecdd3}
655
+
656
+ /* v89: Space Test lives under Active Run in the center column */
657
+ .active-run-panel + .space-test-panel{
658
+ margin-top:0;
659
+ }
660
+ .workspace-grid > .space-test-panel{
661
+ grid-column:2;
662
+ }
663
+ .workspace-grid > .runs-explorer-panel{
664
+ grid-column:3;
665
+ }
666
+ .workspace-grid > .left-stack{
667
+ grid-column:1;
668
+ }
669
+ .workspace-grid > .active-run-panel{
670
+ grid-column:2;
671
+ }
672
+ .space-test-panel .space-test-grid{
673
+ grid-template-columns:1fr;
674
+ gap:12px;
675
+ }
676
+ .space-test-panel .test-top-row{
677
+ grid-template-columns:1fr 120px;
678
+ }
679
+ .space-test-panel .test-top-row label:first-child{
680
+ grid-column:1 / -1;
681
+ }
682
+ .space-test-panel .editor-grid{
683
+ grid-template-columns:1fr;
684
+ }
685
+ .space-test-panel .validation-grid{
686
+ grid-template-columns:1fr 1fr;
687
+ }
688
+ @media (max-width:1180px){
689
+ .workspace-grid > .left-stack,
690
+ .workspace-grid > .active-run-panel,
691
+ .workspace-grid > .space-test-panel,
692
+ .workspace-grid > .runs-explorer-panel{
693
+ grid-column:auto;
694
+ }
695
+ .workspace-grid > .runs-explorer-panel{
696
+ grid-column:1 / -1;
697
+ }
698
+ }
699
+
700
+ /* v90: Pi/provider model mismatch warning */
701
+ .blockers strong:first-child{display:block}
702
+ .blockers p code{font-size:11px}
703
+ .activity-row.warning .event-reason,.activity-row.warn .event-reason{color:#92400e}
web/static/app.js CHANGED
@@ -1,4 +1,4 @@
1
- const state={runId:null,bucketName:'space-factory-runs',bucketSource:null,bucketReady:false,poll:null,activeLinks:{},selectedRunDetail:null,runStatusFilter:'all',runSearch:'',runPage:0,runsPerPage:5,runsCache:[],runsLastLoaded:0,lastRunsRefreshFromPoll:0,lastJobLogPoll:0,pollErrors:0,busy:false,user:null,currentView:'onepage'};
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','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled']);
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
  function saveActiveRun(){try{localStorage.setItem('asf.activeRun',JSON.stringify({runId:state.runId,bucketName:state.bucketName,links:state.activeLinks||{}}))}catch(_){}}
@@ -31,7 +31,9 @@ function setBucketReady(ok,message){state.bucketReady=Boolean(ok);const btn=$('l
31
  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}
32
  function hardwarePlanLabel(){const tryZero=$('tryZeroGpu')?.checked??true;const preferred=effectivePreferredHardware();const fallback=formValue('fallbackHardware')||'a10g-large';const allow=$('allowFallback')?.checked??true;if(tryZero)return `ZeroGPU → ${allow?fallback:'no fixed fallback'}`;return `${preferred}${allow&&fallback!==preferred?' → '+fallback+' fallback':''}`}
33
  function validateBuildForm(){if(!state.bucketReady)return'Create or check your private run bucket before launching a build.';if(!formValue('modelId'))return'Enter a model card URL or model ID.';if(!formValue('targetSpace'))return'Enter a fresh target Space name.';if(!formValue('piModel'))return'Enter the Pi model to use.';return''}
34
- function validateValidationForm(){if(!formValue('validateSpace'))return'Enter the target Space ID to validate.';try{JSON.parse(formValue('testArgs')||'[]');JSON.parse(formValue('testKwargs')||'{}')}catch(e){return`Invalid JSON payload: ${e.message}`}return''}
 
 
35
  function setBuildModeNew(){const panel=$('buildModePanel');if(panel){panel.classList.remove('inspecting');panel.innerHTML='<strong>Preparing a new build</strong><p>This form launches new builds. Existing runs can be inspected in the Runs Explorer on the right.</p>'}const launch=$('launchResultPanel');if(launch)launch.hidden=true;const status=$('progressStatus');if(status){status.textContent='Idle';status.className='badge neutral'}setText('progressTitle','No run selected')}
36
  function setBuildModeInspecting(runId){const panel=$('buildModePanel');if(panel){panel.classList.add('inspecting');panel.innerHTML=`<strong>Inspecting selected run</strong><p>You are viewing run <code>${escapeHtml(runId||'')}</code>. The build form is unchanged and the active run panel is now tracking it.</p>`}}
37
  function resetRunDetails(){
@@ -69,9 +71,83 @@ function renderAuthWarnings(me){const auth=$('authPanel');if(!auth)return;const
69
  async function checkBucket(noisy=true){setButtonBusy('checkBucket',true,'Checking…');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)}}
70
  async function createBucket(){setButtonBusy('createBucket',true,'Creating…');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)}}
71
  function renderManualAction(p={}){const panel=$('manualActionPanel');if(!panel)return;const view=p.view||{};const sm=view.status_model||{};const status=String(p.status||p.state?.status||p.inference_gate?.status||sm.global_status||'');const manual=Boolean(sm.requires_manual_action||p.manual_hardware_required||p.inference_gate?.manual_hardware_required||p.hardware_strategy?.manual_action_required||status.includes('manual_hardware_required')||status.includes('generated_needs_manual_hardware')||status.includes('waiting_manual_action'));panel.hidden=!manual;if(!manual)return;setText('manualActionTitle','Manual hardware required');setText('manualActionText','Automatic hardware selection was not available. Open Space settings, choose the recommended GPU, then run Space Test.');setLink('manualOpenSettings',(p.links||view.links||state.activeLinks||{}).target_space_settings_url)}
72
- function prepareValidationFromActiveRun(){const detail=state.selectedRunDetail;const summary=detail?.summary||{};const target=summary.target_space||$('detailSpace')?.textContent||'';if(target&&target!=='—')setInputValue('validateSpace',target);const expected=summary.expected_output_type||detail?.generation_smoke?.expected_output_type||formValue('buildExpectedOutput')||'image';setInputValue('expectedOutput',expected);setInputValue('validateApi','/generate');switchView('validate');showMessage(`Prepared validation form for ${target||'selected Space'}.`,'info')}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  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,run_id:formValue('runId')||undefined,model_id:formValue('modelId'),target_space_name:formValue('targetSpace'),pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.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)}}
74
- async function launchValidate(){const formError=validateValidationForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching validation Job…');setButtonBusy('launchValidate',true,'Launching…');try{const payload={bucket_name:state.bucketName,target_space_id:formValue('validateSpace'),api_name:formValue('validateApi')||'/generate',expected_output_type:formValue('expectedOutput')||'image',test_args_json:formValue('testArgs')||'[]',test_kwargs_json:formValue('testKwargs')||'{}',live_timeout_seconds:Number(formValue('validateTimeout')||1800)};const result=await apiPost('/api/validate',payload);setActiveRun(result);showMessage(`Validation launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchValidate',false)}}
75
  async function refreshRunReportPreview(runId){
76
  if(!runId)return;
77
  try{
 
1
+ const state={runId:null,validationRunId:null,validationPoll:null,validationPollErrors:0,bucketName:'space-factory-runs',bucketSource:null,bucketReady:false,poll:null,activeLinks:{},selectedRunDetail:null,runStatusFilter:'all',runSearch:'',runPage:0,runsPerPage:5,runsCache:[],runsLastLoaded:0,lastRunsRefreshFromPoll:0,lastJobLogPoll:0,pollErrors:0,busy:false,user:null,currentView:'onepage'};
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','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled']);
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
  function saveActiveRun(){try{localStorage.setItem('asf.activeRun',JSON.stringify({runId:state.runId,bucketName:state.bucketName,links:state.activeLinks||{}}))}catch(_){}}
 
31
  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}
32
  function hardwarePlanLabel(){const tryZero=$('tryZeroGpu')?.checked??true;const preferred=effectivePreferredHardware();const fallback=formValue('fallbackHardware')||'a10g-large';const allow=$('allowFallback')?.checked??true;if(tryZero)return `ZeroGPU → ${allow?fallback:'no fixed fallback'}`;return `${preferred}${allow&&fallback!==preferred?' → '+fallback+' fallback':''}`}
33
  function validateBuildForm(){if(!state.bucketReady)return'Create or check your private run bucket before launching a build.';if(!formValue('modelId'))return'Enter a model card URL or model ID.';if(!formValue('targetSpace'))return'Enter a fresh target Space name.';if(!formValue('piModel'))return'Enter the Pi model to use.';return''}
34
+ function parseJsonField(id,fallback){const raw=formValue(id);if(!raw)return fallback;return JSON.parse(raw)}
35
+ function prettyJson(value){return JSON.stringify(value,null,2)}
36
+ function validateValidationForm(){if(!formValue('validateSpace'))return'Enter the target Space ID to validate.';try{const args=parseJsonField('testArgs',[]);const kwargs=parseJsonField('testKwargs',{});if(!Array.isArray(args))return'Test args must be a JSON array because they are passed as positional arguments to gradio_client.predict(...).';if(!kwargs||Array.isArray(kwargs)||typeof kwargs!=='object')return'Test kwargs must be a JSON object because they are passed as keyword arguments.'}catch(e){return`Invalid JSON payload: ${e.message}`}return''}
37
  function setBuildModeNew(){const panel=$('buildModePanel');if(panel){panel.classList.remove('inspecting');panel.innerHTML='<strong>Preparing a new build</strong><p>This form launches new builds. Existing runs can be inspected in the Runs Explorer on the right.</p>'}const launch=$('launchResultPanel');if(launch)launch.hidden=true;const status=$('progressStatus');if(status){status.textContent='Idle';status.className='badge neutral'}setText('progressTitle','No run selected')}
38
  function setBuildModeInspecting(runId){const panel=$('buildModePanel');if(panel){panel.classList.add('inspecting');panel.innerHTML=`<strong>Inspecting selected run</strong><p>You are viewing run <code>${escapeHtml(runId||'')}</code>. The build form is unchanged and the active run panel is now tracking it.</p>`}}
39
  function resetRunDetails(){
 
71
  async function checkBucket(noisy=true){setButtonBusy('checkBucket',true,'Checking…');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)}}
72
  async function createBucket(){setButtonBusy('createBucket',true,'Creating…');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)}}
73
  function renderManualAction(p={}){const panel=$('manualActionPanel');if(!panel)return;const view=p.view||{};const sm=view.status_model||{};const status=String(p.status||p.state?.status||p.inference_gate?.status||sm.global_status||'');const manual=Boolean(sm.requires_manual_action||p.manual_hardware_required||p.inference_gate?.manual_hardware_required||p.hardware_strategy?.manual_action_required||status.includes('manual_hardware_required')||status.includes('generated_needs_manual_hardware')||status.includes('waiting_manual_action'));panel.hidden=!manual;if(!manual)return;setText('manualActionTitle','Manual hardware required');setText('manualActionText','Automatic hardware selection was not available. Open Space settings, choose the recommended GPU, then run Space Test.');setLink('manualOpenSettings',(p.links||view.links||state.activeLinks||{}).target_space_settings_url)}
74
+ function defaultValidationArgs(expected='text'){
75
+ const kind=String(expected||'text').toLowerCase();
76
+ if(kind==='image')return ["a cinematic robot cat astronaut, detailed, studio lighting"];
77
+ if(kind==='video')return ["a short cinematic shot of a robot cat astronaut walking on the moon"];
78
+ if(kind==='audio')return ["A calm voice saying hello from Agentic Space Factory."];
79
+ return ["Explain the concept of agentic AI in one paragraph."];
80
+ }
81
+ function normalizeApiName(value){
82
+ value=String(value||'').trim();
83
+ if(!value)return'/generate';
84
+ return value.startsWith('/')?value:'/'+value;
85
+ }
86
+ function parsedJsonOrNull(value){
87
+ if(value==null||value==='')return null;
88
+ try{return typeof value==='string'?JSON.parse(value):value}catch(_){return null}
89
+ }
90
+ function validationPrefillFromDetail(detail={}){
91
+ const summary=detail.summary||{};
92
+ const smoke=detail.generation_smoke||detail.tests?.generation_smoke||{};
93
+ const launch=detail.launch||{};
94
+ const stateObj=detail.state||{};
95
+ const expected=smoke.expected_output_type||launch.expected_output_type||summary.expected_output_type||formValue('buildExpectedOutput')||'text';
96
+ let args=Array.isArray(smoke.test_args)?smoke.test_args:parsedJsonOrNull(launch.test_args_json);
97
+ let kwargs=smoke.test_kwargs&&typeof smoke.test_kwargs==='object'&&!Array.isArray(smoke.test_kwargs)?smoke.test_kwargs:parsedJsonOrNull(launch.test_kwargs_json);
98
+ if(!Array.isArray(args))args=defaultValidationArgs(expected);
99
+ if(!kwargs||Array.isArray(kwargs)||typeof kwargs!=='object')kwargs={};
100
+ return {
101
+ target:summary.target_space||stateObj.target_space||launch.target_space||$('detailSpace')?.textContent||'',
102
+ api:normalizeApiName(smoke.api_name||launch.api_name||'/generate'),
103
+ expected,
104
+ args,
105
+ kwargs,
106
+ };
107
+ }
108
+ function renderValidationStatus(data={}){
109
+ const status=data.status||data.view?.header?.status||data.state?.status||'running';
110
+ const badge=$('spaceTestStatus');
111
+ if(badge){badge.textContent=badgeLabel(status);badge.className='badge '+statusClass(status)}
112
+ setText('spaceTestOutput',data.run_id||state.validationRunId||'—');
113
+ setText('lastPolled',formatTime(data.updated_at||data.created_at||new Date().toISOString()));
114
+ const smoke=data.generation_smoke||data.summary?.generation_smoke||{};
115
+ const latency=smoke.latency_seconds||data.summary?.latency_seconds;
116
+ const card=$('spaceTestPreview');
117
+ const latencyNode=card?.querySelector('.validation-grid div:nth-child(2) strong');
118
+ if(latencyNode)latencyNode.textContent=latency?fmtSeconds(Math.round(latency)):'—';
119
+ setText('spaceTestVerdict',status);
120
+ }
121
+ function renderValidationLaunchResult(result={}){
122
+ state.validationRunId=result.run_id;
123
+ renderValidationStatus({...result,status:result.status||'running'});
124
+ }
125
+ function stopValidationPolling(){
126
+ if(state.validationPoll){clearInterval(state.validationPoll);state.validationPoll=null}
127
+ }
128
+ async function refreshValidationProgress(){
129
+ if(!state.validationRunId)return;
130
+ try{
131
+ const p=await apiGet(`/api/runs/${encodeURIComponent(state.validationRunId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}&include_job_logs=1`);
132
+ state.validationPollErrors=0;
133
+ renderValidationStatus(p);
134
+ const status=String(p.view?.header?.status||p.status||p.state?.status||'').toLowerCase();
135
+ if(TERMINAL_STATUSES.has(status)){stopValidationPolling();showMessage(`Validation finished: ${badgeLabel(status)}`,statusClass(status)==='success'?'success':statusClass(status)==='error'?'error':'warning');loadRuns(false)}
136
+ }catch(e){
137
+ state.validationPollErrors+=1;
138
+ console.warn('validation polling failed',e);
139
+ if(state.validationPollErrors<=2)showMessage(`Validation polling failed. Retrying…`,'warning');
140
+ }
141
+ }
142
+ function startValidationPolling(runId){
143
+ state.validationRunId=runId;
144
+ stopValidationPolling();
145
+ refreshValidationProgress();
146
+ state.validationPoll=setInterval(refreshValidationProgress,3000);
147
+ }
148
+ function prepareValidationFromActiveRun(){const detail=state.selectedRunDetail||{};const prefill=validationPrefillFromDetail(detail);if(prefill.target&&prefill.target!=='—')setInputValue('validateSpace',prefill.target);setInputValue('expectedOutput',prefill.expected);setInputValue('validateApi',prefill.api);setInputValue('testArgs',prettyJson(prefill.args));setInputValue('testKwargs',prettyJson(prefill.kwargs));switchView('validate');showMessage(`Prepared validation form for ${prefill.target||'selected Space'}.`,'info')}
149
  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,run_id:formValue('runId')||undefined,model_id:formValue('modelId'),target_space_name:formValue('targetSpace'),pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.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)}}
150
+ 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,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:Number(formValue('validateTimeout')||1800)};const result=await apiPost('/api/validate',payload);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)}}
151
  async function refreshRunReportPreview(runId){
152
  if(!runId)return;
153
  try{
web/static/progress.js CHANGED
@@ -3,7 +3,7 @@ const activityFeedState={lastItems:[],runId:null};
3
  const PROGRESS_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','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled']);
4
 
5
  const WORKER_STEP_ORDER=[
6
- 'bootstrap','dependencies','auth','model_analysis','workspace','node','pi_install','pi_config','pi_run','pi_verification',
7
  'metadata_sanitize','requirements_sanitize','hardware_strategy','create_space_hardware','create_space','repair','upload_files',
8
  'space_runtime','space_logs','api_validation','live_wait','generation_smoke','inference_gate','report_write','done','failure'
9
  ];
@@ -16,7 +16,7 @@ const COMPACT_TIMELINE_GROUPS=[
16
  {id:'workspace_node',label:'Workspace/node',steps:['workspace','node']},
17
  {id:'pi_setup',label:'Pi setup',steps:['pi_install','pi_config']},
18
  {id:'pi_run',label:'Pi run',steps:['pi_run']},
19
- {id:'pi_verify',label:'Pi verify',steps:['pi_verification','metadata_sanitize','requirements_sanitize']},
20
  {id:'hardware',label:'Hardware',steps:['hardware_strategy','create_space_hardware']},
21
  {id:'create_space',label:'Create Space',steps:['create_space']},
22
  {id:'upload_runtime',label:'Upload/runtime',steps:['upload_files','space_runtime','space_logs','repair']},
 
3
  const PROGRESS_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','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled']);
4
 
5
  const WORKER_STEP_ORDER=[
6
+ 'bootstrap','dependencies','auth','model_analysis','workspace','node','pi_install','pi_config','pi_run','pi_model_resolution','pi_verification',
7
  'metadata_sanitize','requirements_sanitize','hardware_strategy','create_space_hardware','create_space','repair','upload_files',
8
  'space_runtime','space_logs','api_validation','live_wait','generation_smoke','inference_gate','report_write','done','failure'
9
  ];
 
16
  {id:'workspace_node',label:'Workspace/node',steps:['workspace','node']},
17
  {id:'pi_setup',label:'Pi setup',steps:['pi_install','pi_config']},
18
  {id:'pi_run',label:'Pi run',steps:['pi_run']},
19
+ {id:'pi_verify',label:'Pi verify',steps:['pi_model_resolution','pi_verification','metadata_sanitize','requirements_sanitize']},
20
  {id:'hardware',label:'Hardware',steps:['hardware_strategy','create_space_hardware']},
21
  {id:'create_space',label:'Create Space',steps:['create_space']},
22
  {id:'upload_runtime',label:'Upload/runtime',steps:['upload_files','space_runtime','space_logs','repair']},
web/static/runs.js CHANGED
@@ -54,5 +54,5 @@ async function deleteSelectedRun(){
54
  }
55
  }
56
  function renderSelectedLinks(summary={},detail={}){const runId=summary.run_id||detail.run_id;const bucket=summary.bucket_source||detail.bucket_source||state.bucketSource;const artifacts=summary.artifacts_url||(typeof artifactUrl==='function'?artifactUrl(runId,bucket):'');const jobUrl=inferredJobUrl(summary,detail);setLink('selectedOpenJob',jobUrl);const ready=typeof runPassedSpaceCreation==='function'?runPassedSpaceCreation({summary,detail,events:detail.events||[]}):Boolean(summary.target_space_url);setLink('selectedOpenSpace',ready?summary.target_space_url:'');setLink('selectedOpenSettings',ready&&summary.target_space_url?summary.target_space_url+'/settings':'');setLink('selectedOpenArtifacts',artifacts);const btn=$('selectedValidateBtn');if(btn){btn.disabled=!summary.target_space||!ready;btn.onclick=prepareValidationFromActiveRun}}
57
- function renderBlockers(detail={}){const root=$('selectedBlockers');if(!root)return;const summary=detail.summary||{};const blockers=detail.technical_blockers||{};const manual=summary.manual_hardware_required||String(summary.status||'').includes('manual_hardware_required');const blockerItems=Array.isArray(blockers.blockers)?blockers.blockers:[];if(!manual&&!blockerItems.length){root.hidden=true;root.innerHTML='';return}root.hidden=false;const parts=[];if(manual)parts.push('<strong>Manual hardware required</strong><p>Open Space settings, choose the recommended GPU, then run validation.</p>');for(const b of blockerItems.slice(0,4)){parts.push(`<strong>${escapeHtml(shortText(b.type||b.name||'Technical blocker',32))}</strong><p>${escapeHtml(shortText(b.claim||b.reason||b.message||'',120))}</p>`)}root.innerHTML=parts.join('')}
58
  function renderRunDetail(detail){state.selectedRunDetail=detail;const summary=detail.summary||{};const smoke=detail.generation_smoke||{};const files=detail.files||[];const view=detail.view||{};const header=view.header||{};setText('selectedRunId',detail.run_id||summary.run_id||'—');setText('selectedModelId',summary.model_id||'—');setText('selectedModelShort',summary.model_id||'—');setText('selectedTargetSpace',summary.target_space||'—');setText('selectedHardware',summary.selected_hardware||summary.hardware||'—');setText('selectedMode',detail.state?.implementation_mode||detail.launch?.implementation_mode||summary.kind||'Strict real inference');setText('runDetailStatus',header.status||summary.status||'unknown');setText('homeRunDetailStatus',header.status_label||summary.status||'unknown');setText('detailRunId',summary.run_id||detail.run_id);setText('detailJobStatus',summary.status||'—');setText('detailGateStatus',(detail.inference_gate||{}).status||summary.status||'—');setText('detailSpace',summary.target_space||'—');setText('detailHardware',summary.selected_hardware||summary.hardware||'—');setText('detailTraces',Array.isArray(detail.events)?String(detail.events.length)+' events':summary.traces_count||'—');const validation=$('selectedValidation');if(validation){validation.innerHTML=`<li><span>Health</span><strong>${summary.health_passed?'passed':'—'}</strong></li><li><span>Smoke test</span><strong>${summary.smoke_test_passed?'passed':(smoke.status||'—')}</strong></li><li><span>Latency</span><strong>${summary.latency_seconds?fmtSeconds(Math.round(summary.latency_seconds)):'—'}</strong></li>`}const artifactRoot=$('artifactList');if(artifactRoot){const interesting=files.filter(f=>!/traces\//.test(f.path||'')).slice(0,18);artifactRoot.innerHTML=interesting.length?interesting.map(f=>`<a href="${escapeHtml(f.url)}" target="_blank"><span>${escapeHtml(f.path)}</span><em>${f.size?Math.round(Number(f.size)/1024)+' KB':''}</em></a>`).join(''):'No artifacts indexed yet.'}const reportText=(detail.report||'No report available.').slice(0,8000);const report=$('reportPreview');if(report)report.textContent=reportText;setQuickLinks({job_url:inferredJobUrl(summary,detail),target_space_url:summary.target_space_url,target_space_settings_url:summary.target_space_url?summary.target_space_url+'/settings':'',artifacts_url:summary.artifacts_url,bucket_source:detail.bucket_source,run_id:summary.run_id||detail.run_id});renderSelectedLinks({...summary,bucket_source:detail.bucket_source},detail);renderBlockers(detail);renderMetrics({...detail,summary});/* v71: avoid rendering stale view.pipeline before /progress returns canonical compact timeline. */const deleteBtn=$('deleteSelectedRun');if(deleteBtn){deleteBtn.disabled=!(summary.run_id||detail.run_id);deleteBtn.onclick=deleteSelectedRun}}
 
54
  }
55
  }
56
  function renderSelectedLinks(summary={},detail={}){const runId=summary.run_id||detail.run_id;const bucket=summary.bucket_source||detail.bucket_source||state.bucketSource;const artifacts=summary.artifacts_url||(typeof artifactUrl==='function'?artifactUrl(runId,bucket):'');const jobUrl=inferredJobUrl(summary,detail);setLink('selectedOpenJob',jobUrl);const ready=typeof runPassedSpaceCreation==='function'?runPassedSpaceCreation({summary,detail,events:detail.events||[]}):Boolean(summary.target_space_url);setLink('selectedOpenSpace',ready?summary.target_space_url:'');setLink('selectedOpenSettings',ready&&summary.target_space_url?summary.target_space_url+'/settings':'');setLink('selectedOpenArtifacts',artifacts);const btn=$('selectedValidateBtn');if(btn){btn.disabled=!summary.target_space||!ready;btn.onclick=prepareValidationFromActiveRun}}
57
+ function renderBlockers(detail={}){const root=$('selectedBlockers');if(!root)return;const summary=detail.summary||{};const blockers=detail.technical_blockers||{};const manual=summary.manual_hardware_required||String(summary.status||'').includes('manual_hardware_required');const blockerItems=Array.isArray(blockers.blockers)?blockers.blockers:[];const piResolution=detail.pi_model_resolution||detail.state?.pi_model_resolution||summary.pi_model_resolution||{};const piMismatch=Boolean(piResolution.mismatch);if(!manual&&!blockerItems.length&&!piMismatch){root.hidden=true;root.innerHTML='';return}root.hidden=false;const parts=[];if(piMismatch)parts.push(`<strong>Pi assistant model changed</strong><p>Requested ${escapeHtml(piResolution.requested_model||'—')}, but Pi/provider traces indicate ${escapeHtml(piResolution.effective_model||'another model')}. This may be an Inference Providers routing/fallback behavior.</p>`);if(manual)parts.push('<strong>Manual hardware required</strong><p>Open Space settings, choose the recommended GPU, then run validation.</p>');for(const b of blockerItems.slice(0,4)){parts.push(`<strong>${escapeHtml(shortText(b.type||b.name||'Technical blocker',32))}</strong><p>${escapeHtml(shortText(b.claim||b.reason||b.message||'',120))}</p>`)}root.innerHTML=parts.join('')}
58
  function renderRunDetail(detail){state.selectedRunDetail=detail;const summary=detail.summary||{};const smoke=detail.generation_smoke||{};const files=detail.files||[];const view=detail.view||{};const header=view.header||{};setText('selectedRunId',detail.run_id||summary.run_id||'—');setText('selectedModelId',summary.model_id||'—');setText('selectedModelShort',summary.model_id||'—');setText('selectedTargetSpace',summary.target_space||'—');setText('selectedHardware',summary.selected_hardware||summary.hardware||'—');setText('selectedMode',detail.state?.implementation_mode||detail.launch?.implementation_mode||summary.kind||'Strict real inference');setText('runDetailStatus',header.status||summary.status||'unknown');setText('homeRunDetailStatus',header.status_label||summary.status||'unknown');setText('detailRunId',summary.run_id||detail.run_id);setText('detailJobStatus',summary.status||'—');setText('detailGateStatus',(detail.inference_gate||{}).status||summary.status||'—');setText('detailSpace',summary.target_space||'—');setText('detailHardware',summary.selected_hardware||summary.hardware||'—');setText('detailTraces',Array.isArray(detail.events)?String(detail.events.length)+' events':summary.traces_count||'—');const validation=$('selectedValidation');if(validation){validation.innerHTML=`<li><span>Health</span><strong>${summary.health_passed?'passed':'—'}</strong></li><li><span>Smoke test</span><strong>${summary.smoke_test_passed?'passed':(smoke.status||'—')}</strong></li><li><span>Latency</span><strong>${summary.latency_seconds?fmtSeconds(Math.round(summary.latency_seconds)):'—'}</strong></li>`}const artifactRoot=$('artifactList');if(artifactRoot){const interesting=files.filter(f=>!/traces\//.test(f.path||'')).slice(0,18);artifactRoot.innerHTML=interesting.length?interesting.map(f=>`<a href="${escapeHtml(f.url)}" target="_blank"><span>${escapeHtml(f.path)}</span><em>${f.size?Math.round(Number(f.size)/1024)+' KB':''}</em></a>`).join(''):'No artifacts indexed yet.'}const reportText=(detail.report||'No report available.').slice(0,8000);const report=$('reportPreview');if(report)report.textContent=reportText;setQuickLinks({job_url:inferredJobUrl(summary,detail),target_space_url:summary.target_space_url,target_space_settings_url:summary.target_space_url?summary.target_space_url+'/settings':'',artifacts_url:summary.artifacts_url,bucket_source:detail.bucket_source,run_id:summary.run_id||detail.run_id});renderSelectedLinks({...summary,bucket_source:detail.bucket_source},detail);renderBlockers(detail);renderMetrics({...detail,summary});/* v71: avoid rendering stale view.pipeline before /progress returns canonical compact timeline. */const deleteBtn=$('deleteSelectedRun');if(deleteBtn){deleteBtn.disabled=!(summary.run_id||detail.run_id);deleteBtn.onclick=deleteSelectedRun}}