File size: 23,755 Bytes
d6ef0ec 8be4404 4b9e3fd 7584325 a02cf72 d6ef0ec 65a9f09 8be4404 a02cf72 8be4404 7584325 8be4404 a02cf72 7584325 8be4404 d6ef0ec 8be4404 fcf6644 7584325 8be4404 c23f51b a02cf72 8be4404 7584325 8be4404 d6ef0ec 7584325 8be4404 fcf6644 | 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 | const state={runId:null,bucketName:'space-factory-runs',bucketSource:null,bucketReady:false,poll:null,activeLinks:{},selectedRunDetail:null,runStatusFilter:'all',runSearch:'',runsCache:[],runsLastLoaded:0,pollErrors:0,busy:false,user:null,currentView:'onepage'};
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']);
const DEFAULT_TIMELINE=[{label:'Bucket ready',status:'pending'},{label:'Job launched',status:'pending'},{label:'Model analysis',status:'pending'},{label:'Generate app',status:'pending'},{label:'Create Space',status:'pending'},{label:'Upload files',status:'pending'},{label:'Hardware',status:'pending'},{label:'Live validation',status:'pending'},{label:'Report',status:'pending'}];
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 artifactUrl(runId,bucketSource){if(!runId||!bucketSource)return'';return `https://huggingface.co/buckets/${bucketSource}/tree/runs/${runId}`}
function ownerFromBucketSource(bucket){return bucket?String(bucket).split('/')[0]:''}
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.created_by||out.owner||out.username||state.user?.username||ownerFromBucketSource(bucket);if(jobId&&owner)out.job_url=`https://huggingface.co/jobs/${owner}/${jobId}`}return out}
function setQuickLinks(links={}){links=normalizeLinks(links||{});state.activeLinks=links;for(const id of ['openJob','selectedOpenJob'])setLink(id,links.job_url);for(const id of ['openSpace','selectedOpenSpace'])setLink(id,links.target_space_url);for(const id of ['openSettings','selectedOpenSettings','manualOpenSettings'])setLink(id,links.target_space_settings_url||links.target_space_url&&links.target_space_url+'/settings');for(const id of ['openArtifacts','selectedOpenArtifacts'])setLink(id,links.artifacts_url);renderManualAction({links})}
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: ${state.bucketSource||'connected'}`:'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 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')}
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>`}}
function resetRunDetails(){for(const [id,value] of [['detailRunId','—'],['detailJobStatus','—'],['detailGateStatus','—'],['detailSpace','—'],['detailHardware','—'],['detailTraces','—'],['homeRunDetailStatus','No run'],['selectedRunId','—'],['selectedModelId','—'],['selectedTargetSpace','—'],['selectedHardware','—'],['runDetailStatus','—']])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 full=$('reportPreview');if(full)full.textContent='No report selected.';const manual=$('manualActionPanel');if(manual)manual.hidden=true;const activity=$('activityFeed');if(activity)activity.innerHTML='<div class="empty">No activity yet.</div>'}
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');setText('lastEvent','Choose a model card and launch a build.');setText('elapsed','—');setText('lastPolled','—');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')}
function renderLaunchResult(result={}){const panel=$('launchResultPanel');if(panel)panel.hidden=false;setText('launchRunId',result.run_id||'—');setText('launchStatus',result.status||'running');setText('launchSpace',result.target_space||'—');setText('launchHardware',result.selected_hardware||result.preferred_space_hardware||'—');const links=normalizeLinks({...result.links,bucket_source:result.bucket_source,run_id:result.run_id,job_id:result.job_id,job_owner:state.user?.username,target_space_url:result.target_space_url});setLink('launchOpenJob',links.job_url);setLink('launchOpenSpace',links.target_space_url);setLink('launchOpenSettings',links.target_space_settings_url||links.target_space_url&&links.target_space_url+'/settings');setLink('launchOpenArtifacts',links.artifacts_url)}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,target_space_url:result.target_space_url});renderLaunchResult(result);saveActiveRun();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 bootstrapRunRecovery(){return restoreActiveRun()}
async function restoreActiveRun(){try{const resumable=await apiGet(`/api/runs/resumable?bucket_name=${encodeURIComponent(state.bucketName)}`);if(resumable?.run?.run_id){state.runId=resumable.run.run_id;if(resumable.bucket_source)state.bucketSource=resumable.bucket_source;setBuildModeInspecting(state.runId);if(resumable.view)renderRunViewModel(resumable.view,{progress:0,events:[]});setQuickLinks(resumable.view?.links||{});startPolling(state.runId);showMessage(`Resumed latest active run: ${state.runId}`,'info');return true}}catch(e){console.warn('resumable lookup failed',e)}try{const raw=localStorage.getItem('asf.activeRun');if(!raw)return false;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);return true}}catch(_){}return false}
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'}state.bucketSource=me.username+'/'+state.bucketName;setText('bucketInlineLabel',state.bucketSource);renderAuthWarnings(me);await checkBucket(false);await loadRuns(true);await bootstrapRunRecovery();if(!state.bucketReady)setOperation('Create your private run bucket before launching builds.','warning');else setOperation('')}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…');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)}}
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)}}
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)}
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')}
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)}}
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)}}
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);if(p.state||p.summary){if(p.bucket_source)state.bucketSource=p.bucket_source;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.view?.header?.status_label||p.state?.status||p.status||'running');setQuickLinks({...p.links,...p.view?.links,bucket_source:p.bucket_source,run_id:p.run_id});saveActiveRun()}const status=String(p.view?.header?.status||p.status||p.state?.status||'').toLowerCase();if(TERMINAL_STATUSES.has(status)&&state.poll){clearInterval(state.poll);state.poll=null;setOperation(`Run finished with status: ${badgeLabel(status)}`,status.includes('success')||status.includes('succeed')?'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||p.summary?.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.'}
async function resumeLatestRun(){try{const data=await apiGet(`/api/runs/resumable?bucket_name=${encodeURIComponent(state.bucketName)}`);if(!data.run){showMessage('No resumable run found in this bucket.','warning');return}state.runId=data.run.run_id;if(data.bucket_source)state.bucketSource=data.bucket_source;if(data.view)renderRunViewModel(data.view,{progress:0,events:[]});setBuildModeInspecting(state.runId);setQuickLinks(data.view?.links||{});startPolling(state.runId);showMessage(`Resumed ${state.runId}`,'success')}catch(e){showMessage(e.message,true)}}
function switchView(view){state.currentView=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;if(view==='progress')loadRuns(false);const targetMap={build:'newBuildCard',progress:'runsExplorerPanel',validate:'spaceTestSection'};const target=$(targetMap[view]||'');if(target)target.scrollIntoView({behavior:'smooth',block:'start'})}
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';renderRunsFromCache()}));for(const id of ['runSearch','runSearchFull'])$(id)?.addEventListener('input',e=>{state.runSearch=e.target.value||'';for(const other of ['runSearch','runSearchFull'])if($(other)&&$(other)!==e.target)$(other).value=state.runSearch;clearTimeout(state.runSearchTimer);state.runSearchTimer=setTimeout(renderRunsFromCache,80)})}
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);$('selectedValidateBtn')?.addEventListener('click',prepareValidationFromActiveRun);$('checkBucket')?.addEventListener('click',()=>checkBucket(true));$('bucketStatusPill')?.addEventListener('click',()=>checkBucket(true));$('createBucket')?.addEventListener('click',createBucket);$('refreshRuns')?.addEventListener('click',()=>loadRuns(true));$('refreshRunsHome')?.addEventListener('click',()=>loadRuns(true));$('resumeLatestRun')?.addEventListener('click',resumeLatestRun);$('resumeLatestRunFull')?.addEventListener('click',resumeLatestRun);['tryZeroGpu','preferredHardware','fallbackHardware','allowFallback'].forEach(id=>$(id)?.addEventListener('change',()=>setText('hardwarePlan',hardwarePlanLabel())))}
bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());setBuildModeNew();renderTimeline(DEFAULT_TIMELINE);loadAppInfo();
// Pass 47 intentionally removed automatic switchView('progress') after launch and run selection.
|