File size: 20,904 Bytes
a02cf72
 
 
 
 
 
 
 
 
 
 
 
 
f2c702d
a02cf72
7584325
a02cf72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7584325
 
 
 
 
 
a02cf72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7584325
a02cf72
 
 
 
 
 
 
 
 
 
 
7584325
 
 
a02cf72
f2c702d
a02cf72
f2c702d
b20b865
a02cf72
 
 
 
 
 
 
 
 
 
 
 
f2c702d
7584325
f2c702d
c23f51b
a02cf72
 
 
 
7584325
a02cf72
 
 
7584325
 
a02cf72
7584325
a02cf72
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
const state={
  runId:null,
  bucketName:'space-factory-runs',
  bucketSource:null,
  bucketReady:false,
  poll:null,
  activeLinks:{},
  selectedRunDetail:null,
  runStatusFilter:'all',
  runsCache:[],
  runsLastLoaded:0,
  pollErrors:0,
  busy:false,
  user:null,
};
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']);
const DEFAULT_TIMELINE=[
  {label:'Bucket ready',status:'pending'},
  {label:'Job launched',status:'pending'},
  {label:'Dependencies',status:'pending'},
  {label:'Authenticated',status:'pending'},
  {label:'Model analysis',status:'pending'},
  {label:'Workspace',status:'pending'},
  {label:'Pi installed',status:'pending'},
  {label:'Pi running',status:'pending'},
  {label:'Create Space',status:'pending'},
  {label:'Upload files',status:'pending'},
  {label:'Hardware request',status:'pending'},
  {label:'API validation',status:'pending'},
  {label:'Smoke test',status:'pending'},
  {label:'Done',status:'pending'},
];
function $(id){return document.getElementById(id)}
function formValue(id){return ($(id)?.value||'').trim()}
function setText(id,value){const el=$(id);if(el)el.textContent=value??'—'}
function setInputValue(id,value){const el=$(id);if(!el)return;el.value=value||''}
function escapeHtml(value){return String(value??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]))}
function saveActiveRun(){try{localStorage.setItem('asf.activeRun',JSON.stringify({runId:state.runId,bucketName:state.bucketName,links:state.activeLinks||{}}))}catch(_){}}
function clearSavedRun(){try{localStorage.removeItem('asf.activeRun')}catch(_){}}
function restoreActiveRun(){try{const raw=localStorage.getItem('asf.activeRun');if(!raw)return;const saved=JSON.parse(raw);if(saved?.bucketName)state.bucketName=saved.bucketName;if(saved?.runId){state.runId=saved.runId;setText('detailRunId',saved.runId);setQuickLinks(saved.links||{});setBuildModeInspecting(saved.runId);startPolling(saved.runId)}}catch(_){}}
let operationTimer=null;
function setOperation(message,type='info'){
  const el=$('operationStatus');if(!el)return;
  if(operationTimer){clearTimeout(operationTimer);operationTimer=null}
  if(!message){el.hidden=true;el.textContent='';el.className='operation-status';return}
  el.hidden=false;el.className='operation-status '+type;el.textContent=message;
  if(type==='success'||type==='info')operationTimer=setTimeout(()=>{if(!state.busy)setOperation('')},7000)
}
function showMessage(msg,isError=false){const type=isError===true?'error':(typeof isError==='string'?isError:'info');setOperation(msg,type);const el=$('eventsPanel');if(el&&!state.runId)el.textContent=(isError?'Error: ':'')+msg;const warn=$('pollWarning');if(warn&&isError){warn.textContent=msg;warn.hidden=false}}
function setBusy(isBusy,label='Working…'){
  state.busy=Boolean(isBusy);
  document.querySelectorAll('[data-action-button]').forEach(btn=>{btn.disabled=state.busy||btn.dataset.requiresBucket==='true'&&!state.bucketReady});
  if($('launchBuild'))$('launchBuild').disabled=state.busy||!state.bucketReady;
  if($('launchValidate'))$('launchValidate').disabled=state.busy;
  if(state.busy)setOperation(label,'loading')
}
function setButtonBusy(id,isBusy,busyLabel){const btn=$(id);if(!btn)return;if(isBusy){btn.dataset.oldText=btn.textContent;btn.textContent=busyLabel||'Working…';btn.disabled=true;btn.classList.add('is-busy')}else{if(btn.dataset.oldText)btn.textContent=btn.dataset.oldText;btn.disabled=state.busy||btn.dataset.requiresBucket==='true'&&!state.bucketReady;btn.classList.remove('is-busy')}}
function setBucketReady(ok,message){
  state.bucketReady=Boolean(ok);
  const btn=$('launchBuild');if(btn)btn.disabled=state.busy||!state.bucketReady;
  setText('buildGateText',state.bucketReady?'Bucket ready. You can launch builds.':(message||'Create your private run bucket before launching builds.'));
  setText('bucketStatusText',state.bucketReady?'Bucket ready':'Bucket not ready');
  setText('bucketInlineStatus',state.bucketReady?'Bucket ready':'Not ready');
  for(const id of ['bucketStatusDot','bucketInlineDot','sidebarBucketDot']){const dot=$(id);if(dot)dot.className='status-dot '+(state.bucketReady?'green':'amber-dot')}
  const inline=$('bucketInlineStatus');if(inline)inline.className='badge '+(state.bucketReady?'success':'warn');
  document.querySelectorAll('[data-requires-bucket="true"]').forEach(btn=>btn.disabled=state.busy||!state.bucketReady)
}
function effectivePreferredHardware(){const preferred=formValue('preferredHardware')||'zero-a10g';const fallback=formValue('fallbackHardware')||'l40sx1';const tryZero=$('tryZeroGpu')?.checked??true;if(tryZero)return preferred;if(preferred==='zero-a10g'||preferred.startsWith('zero'))return fallback;return preferred}
function hardwarePlanLabel(){const tryZero=$('tryZeroGpu')?.checked??true;const preferred=effectivePreferredHardware();const fallback=formValue('fallbackHardware')||'l40sx1';const allow=$('allowFallback')?.checked??true;if(tryZero)return `ZeroGPU → ${allow?fallback:'no fixed fallback'}`;return `${preferred}${allow&&fallback!==preferred?' → '+fallback+' fallback':''}`}
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''}
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''}
function artifactUrl(runId,bucketSource){if(!runId||!bucketSource)return'';return `https://huggingface.co/buckets/${bucketSource}/tree/main/runs/${runId}`}
function normalizeLinks(links={}){const out={...links};const bucket=out.bucket_source||state.bucketSource;const run=out.run_id||state.runId;if(!out.artifacts_url&&bucket&&run)out.artifacts_url=artifactUrl(run,bucket);if(!out.job_url){const jobId=out.job_id||out.jobId;const owner=out.job_owner||out.owner||out.username||state.user?.username;if(jobId&&owner)out.job_url=`https://huggingface.co/jobs/${owner}/${jobId}`;}return out}
function setQuickLinks(links={}){links=normalizeLinks(links||{});state.activeLinks=links;setLink('openJob',links.job_url);setLink('openSpace',links.target_space_url);setLink('openSettings',links.target_space_settings_url||links.target_space_url&&links.target_space_url+'/settings');setLink('openArtifacts',links.artifacts_url);setLink('manualOpenSettings',links.target_space_settings_url||links.target_space_url&&links.target_space_url+'/settings');renderManualAction({links})}
function setBuildModeNew(){const panel=$('buildModePanel');if(panel){panel.classList.remove('inspecting');panel.innerHTML='<strong>Preparing a new build</strong><p>This form always starts a fresh private Space build. Selecting a run in Live progress only changes the progress and detail panels.</p>'}setText('progressTitle','Live job progress');setText('progressStatus','Idle')}
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. Open New Build and click “Prepare new build” to clear the run context.</p>`}}
function resetRunDetails(){
  for(const [id,value] of [['detailRunId','—'],['detailJobStatus','—'],['detailGateStatus','—'],['detailSpace','—'],['detailHardware','—'],['detailTraces','—'],['homeRunDetailStatus','No run']])setText(id,value);
  setQuickLinks({});
  const report=$('homeReportPreview');if(report)report.innerHTML='<strong>No run selected</strong><p>Select a run to preview its report and actions.</p>';
  const manual=$('manualActionPanel');if(manual)manual.hidden=true;
}
function prepareNewBuild(){
  if(state.poll){clearInterval(state.poll);state.poll=null}
  state.runId=null;state.selectedRunDetail=null;state.activeLinks={};state.pollErrors=0;clearSavedRun();
  document.querySelectorAll('.run-row').forEach(r=>r.classList.remove('selected'));
  setBuildModeNew();resetRunDetails();renderTimeline(DEFAULT_TIMELINE);setText('progressPercent','0%');const fill=$('progressFill');if(fill)fill.style.width='0%';setText('currentStep','Ready for a new build');setText('lastEvent','Choose a model card and launch a build.');setText('elapsed','—');setText('lastPolled','—');setText('hardwarePlan',hardwarePlanLabel());renderEvents([]);renderMetrics({});showMessage('New build mode ready. The form is independent from selected runs.','info')
}
function setActiveRun(result){state.runId=result.run_id;if(result.bucket_source){state.bucketSource=result.bucket_source}setText('detailRunId',result.run_id);setText('detailJobStatus',result.status||'RUNNING');setText('detailGateStatus',result.kind||'running');setText('detailSpace',result.target_space||'—');setText('detailHardware',result.selected_hardware||result.preferred_space_hardware||'—');setText('homeRunDetailStatus',result.status||'Running');setBuildModeInspecting(result.run_id);setQuickLinks({...result.links,bucket_source:result.bucket_source,run_id:result.run_id,job_id:result.job_id,job_owner:state.user?.username});saveActiveRun();switchView('progress');startPolling(result.run_id)}
function startPolling(runId){state.runId=runId;state.pollErrors=0;const warn=$('pollWarning');if(warn)warn.hidden=true;if(state.poll)clearInterval(state.poll);saveActiveRun();refreshProgress();state.poll=setInterval(refreshProgress,4000)}
async function loadAppInfo(){setOperation('Checking sign-in and run storage…','loading');try{const me=await apiGet('/api/me');state.user=me;setText('userChip','@'+me.username);const logout=$('logoutLink');if(logout){logout.hidden=false;logout.href=me.logout_url||'/logout'}setText('bucketLabel','Bucket: '+me.username+'/'+state.bucketName);setText('bucketInlineLabel',me.username+'/'+state.bucketName);renderAuthWarnings(me);await checkBucket(false);restoreActiveRun();if(!state.bucketReady)setOperation('Create your private run bucket before launching builds.','warning');else setOperation('');loadRuns(true)}catch(e){state.user=null;setText('userChip','Sign in');const logout=$('logoutLink');if(logout)logout.hidden=true;renderSignedOut();setBucketReady(false,'Sign in with Hugging Face first.');showMessage('Please sign in with Hugging Face, then refresh this page.',true)}}
function renderSignedOut(){const auth=$('authPanel');if(!auth)return;auth.hidden=false;auth.className='auth-panel warning';auth.innerHTML='<strong>Sign in required</strong><p>Use Hugging Face OAuth before launching Jobs or reading your Bucket.</p><a class="link-btn" href="/oauth/huggingface/login">Sign in with Hugging Face ↗</a>'}
function renderAuthWarnings(me){const auth=$('authPanel');if(!auth)return;const missing=me.missing_scopes||[];const warnings=me.warnings||[];if(!missing.length&&!warnings.length){auth.hidden=true;auth.innerHTML='';return}auth.hidden=false;auth.className='auth-panel warning';auth.innerHTML=`<strong>OAuth session warning</strong><p>${escapeHtml([...warnings,missing.length?'Missing scopes: '+missing.join(', '):''].filter(Boolean).join(' • '))}</p><a class="link-btn" href="/oauth/huggingface/login">Refresh sign-in ↗</a>`}
async function checkBucket(noisy=true){setButtonBusy('checkBucket',true,'Checking…');setButtonBusy('checkBucketSide',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('bucketLabel','Bucket: '+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);setButtonBusy('checkBucketSide',false)}}
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('bucketLabel','Bucket: '+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)}}
function renderManualAction(p={}){const panel=$('manualActionPanel');if(!panel)return;const status=String(p.status||p.state?.status||p.inference_gate?.status||'');const manual=Boolean(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'));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 Test Space.');setLink('manualOpenSettings',(p.links||state.activeLinks||{}).target_space_settings_url)}
function prepareValidationFromActiveRun(){const detail=state.selectedRunDetail;const summary=detail?.summary||{};const target=summary.target_space||document.getElementById('detailSpace')?.textContent||'';if(target&&target!=='—')setInputValue('validateSpace',target);const expected=summary.expected_output_type||detail?.generation_smoke?.expected_output_type||'image';setInputValue('expectedOutput',expected);setInputValue('validateApi','/generate');switchView('validate');showMessage(`Prepared validation form for ${target||'selected Space'}.`,'info')}
async function launchBuild(){const formError=validateBuildForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching build Job…');setButtonBusy('launchBuild',true,'Launching build…');try{setText('hardwarePlan',hardwarePlanLabel());const payload={bucket_name:state.bucketName,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};const result=await apiPost('/api/build',payload);setActiveRun(result);showMessage(`Build launched: ${result.run_id}`);loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchBuild',false)}}
async function launchValidate(){const formError=validateValidationForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching validation Job…');setButtonBusy('launchValidate',true,'Launching validation…');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}`);loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchValidate',false)}}
async function refreshProgress(){if(!state.runId)return;try{const p=await apiGet(`/api/runs/${encodeURIComponent(state.runId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}`);state.pollErrors=0;const warn=$('pollWarning');if(warn)warn.hidden=true;renderProgress(p);renderEvents(p.events||[]);renderMetrics(p);renderManualAction(p);if(p.state){setText('detailJobStatus',p.state.status||p.status||'running');setText('detailGateStatus',p.inference_gate?.status||p.state.status||p.status||'—');setText('detailSpace',p.state.target_space||p.summary?.target_space||'—');setText('detailHardware',p.summary?.selected_hardware||p.state.selected_hardware||p.hardware_strategy?.selected_hardware||'—');setText('detailTraces',Array.isArray(p.events)?String(p.events.length)+' events':'—');setText('homeRunDetailStatus',p.state.status||p.status||'running');if(p.bucket_source)state.bucketSource=p.bucket_source;setQuickLinks({...p.links,bucket_source:p.bucket_source,run_id:p.run_id});saveActiveRun()}const status=String(p.status||p.state?.status||'').toLowerCase();if(TERMINAL_STATUSES.has(status)&&state.poll){clearInterval(state.poll);state.poll=null;setOperation(`Run finished with status: ${status}`,status.includes('success')?'success':status.includes('failed')||status.includes('blocker')?'error':'warning')}loadRuns(false)}catch(e){state.pollErrors+=1;console.warn(e);const warn=$('pollWarning');if(warn){warn.textContent=`Progress polling failed ${state.pollErrors} time${state.pollErrors===1?'':'s'}. The Job may still be running; retrying automatically…`;warn.hidden=false}}}
function renderMetrics(p){const root=$('metricsList');if(!root)return;const smoke=p.generation_smoke||{};const gate=p.inference_gate||{};const health=(gate.implementation_signals&&gate.implementation_signals.health_passed)||smoke.health_passed;const smokeOk=smoke.ok||smoke.status==='success';const latency=smoke.latency_seconds;const duration=smoke.recommended_zerogpu_duration_seconds||smoke.recommended_duration_seconds;root.innerHTML=`<li><span>/health</span><strong class="${health?'metric-ok':''}">${health?'passed':'pending'}</strong></li><li><span>/generate smoke test</span><strong class="${smokeOk?'metric-ok':'metric-warn'}">${smokeOk?'passed':(smoke.status||'pending')}</strong></li><li><span>Latency</span><strong>${latency?fmtSeconds(Math.round(latency)):'—'}</strong></li><li><span>Recommended ZeroGPU duration</span><strong>${duration?fmtSeconds(Math.round(duration)):'—'}</strong></li>`}
function renderEvents(events){const root=$('eventsPanel');if(!root)return;root.textContent=(events||[]).slice(-12).map(e=>`${e.ts||''}  ${e.step||''}  ${e.status||''}  ${e.message||''}`).join('\n')||'No events yet. The run may still be scheduling.'}
function switchView(view){document.querySelectorAll('.nav-item').forEach(b=>b.classList.toggle('active',b.dataset.view===view));document.querySelectorAll('[data-section]').forEach(s=>s.hidden=s.dataset.section!==view);setOperation('');const warn=$('pollWarning');if(warn)warn.hidden=true}
function bindNavigation(){document.querySelectorAll('.nav-item').forEach(btn=>btn.addEventListener('click',()=>switchView(btn.dataset.view||'build')));document.querySelectorAll('[data-view-target]').forEach(btn=>btn.addEventListener('click',()=>switchView(btn.dataset.viewTarget||'build')))}
function bindRunFilters(){document.querySelectorAll('.filter[data-status]').forEach(btn=>btn.addEventListener('click',()=>{state.runStatusFilter=btn.dataset.status||'all';document.querySelectorAll('.filter[data-status]').forEach(x=>x.classList.toggle('active',x.dataset.status===state.runStatusFilter));renderRunsFromCache()}))}
function bindActions(){
  $('launchBuild')?.setAttribute('data-requires-bucket','true');$('launchBuild')?.setAttribute('data-action-button','true');$('launchValidate')?.setAttribute('data-action-button','true');
  $('launchBuild')?.addEventListener('click',launchBuild);$('launchValidate')?.addEventListener('click',launchValidate);$('prepareNewBuild')?.addEventListener('click',prepareNewBuild);
  $('manualGoValidate')?.addEventListener('click',prepareValidationFromActiveRun);$('homeValidateBtn')?.addEventListener('click',prepareValidationFromActiveRun);
  $('checkBucket')?.addEventListener('click',()=>checkBucket(true));$('createBucket')?.addEventListener('click',createBucket);$('checkBucketSide')?.addEventListener('click',()=>checkBucket(true));
  $('refreshRuns')?.addEventListener('click',()=>loadRuns(true));$('refreshRunsHome')?.addEventListener('click',()=>loadRuns(true));
  ['tryZeroGpu','preferredHardware','fallbackHardware','allowFallback'].forEach(id=>$(id)?.addEventListener('change',()=>setText('hardwarePlan',hardwarePlanLabel())))
}
bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());setBuildModeNew();loadAppInfo();renderTimeline(DEFAULT_TIMELINE);