File size: 61,686 Bytes
1b50886
56b58d8
 
c843485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7584325
bc18c93
d6ef0ec
65a9f09
 
2af92fd
 
 
 
 
 
 
 
 
bc18c93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c843485
 
 
 
 
 
 
 
 
 
 
bc18c93
 
c843485
 
 
 
 
 
 
 
 
 
 
 
 
1b50886
c843485
 
 
 
 
 
 
 
 
 
bc18c93
1b50886
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc18c93
 
 
c843485
bc18c93
1b50886
bc18c93
e3f778e
c1f7b6c
b353c3a
 
 
1b50886
 
e3f778e
bc18c93
2af92fd
 
 
bc18c93
 
2af92fd
bc18c93
2af92fd
 
 
c843485
 
bc18c93
 
 
 
 
c843485
5660fa3
 
c843485
 
 
 
e3f778e
 
 
 
 
 
 
 
 
 
 
06436a6
e3f778e
06436a6
e3f778e
c843485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3f778e
 
 
c843485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3f778e
c843485
 
 
 
 
1b50886
c843485
 
1b50886
c2bd1f6
 
 
d6ef0ec
 
d1586da
 
690ac59
c778235
d1586da
 
bc18c93
d1586da
 
 
 
 
 
 
 
2af92fd
d1586da
c778235
d1586da
 
91e71d6
bc18c93
d1586da
bc18c93
d1586da
1da73cf
cea6366
25bbd6e
 
 
 
b353c3a
 
 
 
25bbd6e
 
 
 
 
 
 
 
 
 
8be4404
2af92fd
bc18c93
c23f51b
a02cf72
bc18c93
25bbd6e
bc18c93
1b50886
25bbd6e
 
 
bc18c93
 
25bbd6e
bc18c93
 
 
 
 
 
1da73cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8be4404
c2bd1f6
 
 
 
 
 
 
 
 
 
 
 
c843485
 
 
 
 
 
 
 
 
 
 
 
 
 
c2bd1f6
 
 
 
 
06436a6
 
 
 
 
 
 
c2bd1f6
 
 
 
06436a6
 
 
c2bd1f6
 
bc18c93
 
 
 
1da73cf
 
 
c2bd1f6
1da73cf
 
 
 
 
bc18c93
 
 
c843485
 
1da73cf
bc18c93
 
 
 
 
 
 
 
 
 
 
c2bd1f6
 
 
 
 
bc18c93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c2bd1f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1da73cf
c2bd1f6
 
 
 
1da73cf
 
 
 
 
 
 
bc18c93
1da73cf
 
 
06436a6
 
1da73cf
 
 
 
 
1b50886
c2bd1f6
e15e363
 
 
c1f7b6c
e15e363
 
bc18c93
cea6366
 
 
 
 
bc18c93
 
 
 
e15e363
 
 
 
cea6366
25bbd6e
 
 
 
 
 
 
 
 
 
 
 
 
 
c843485
 
 
25bbd6e
40bd1f5
25bbd6e
40bd1f5
25bbd6e
c843485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cea6366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c843485
e477c9c
56b58d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1da73cf
c843485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fcf6644
 
c843485
1b50886
 
e3f778e
 
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
const state={runId:null,validationRunId:null,validationPoll:null,centerTab:'active',validationPollErrors:0,validationDetail:null,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};
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']);
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'}];

function bindOnce(el,eventName,key,handler,options){
  if(!el)return;
  const attr='bound'+String(key||eventName).replace(/[^a-zA-Z0-9]/g,'');
  if(el.dataset[attr]==='true')return;
  el.dataset[attr]='true';
  el.addEventListener(eventName,handler,options);
}
function bindAllOnce(selector,eventName,key,handler,options){
  document.querySelectorAll(selector).forEach((el,index)=>bindOnce(el,eventName,`${key}${index}`,handler,options));
}

function updateImplementationModeHelp(){
  const value=formValue('implementationMode')||'full-inference-gated';
  const copy=implementationModeCopy(value);
  const el=$('implementationModeHelp');
  if(!el)return;
  el.innerHTML=`<strong>${escapeHtml(copy.title)}</strong><span>${escapeHtml(copy.description)}</span>`;
}

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(_){} state.spaceLinksUnlockedFor=null}
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 runPassedSpaceCreation(payload={}){
  const events=payload.events||payload.view?.activity||payload.detail?.events||[];
  const summary=payload.summary||payload.detail?.summary||{};
  const stateObj=payload.state||payload.detail?.state||{};
  const success=String(payload.status||summary.status||stateObj.status||'').toLowerCase();
  if(success.includes('success')||success.includes('succeed')||success==='done'||success==='completed')return true;
  const passedSteps=new Set(['create_space','upload_files','space_runtime','space_logs','api_validation','live_wait','generation_smoke','inference_gate','report_write','done']);
  return Array.isArray(events)&&events.some(e=>passedSteps.has(String(e.step||''))&&String(e.status||'').toLowerCase()!=='started');
}

function isValidationRunDetail(detail={}){
  const summary=detail.summary||{};
  const launch=detail.launch||{};
  const stateObj=detail.state||{};
  const view=detail.view||{};
  const values=[detail.kind,summary.kind,launch.kind,stateObj.kind,view.kind,summary.run_type,launch.run_type,stateObj.run_type,detail.run_type];
  return values.some(v=>String(v||'').toLowerCase().includes('validate_existing_space')||String(v||'').toLowerCase().includes('validation'));
}
function validationTimelineItems(detail={}){
  const events=detail.events||detail.view?.activity||[];
  if(Array.isArray(detail.timeline)&&detail.timeline.length)return detail.timeline;
  if(Array.isArray(detail.progress?.timeline)&&detail.progress.timeline.length)return detail.progress.timeline;
  if(!Array.isArray(events)||!events.length)return [
    {step:'bootstrap',label:'Bootstrap',status:'pending'},
    {step:'dependencies',label:'Dependencies',status:'pending'},
    {step:'auth',label:'Auth',status:'pending'},
    {step:'live_wait',label:'Live wait',status:'pending'},
    {step:'generation_smoke',label:'Generation smoke',status:'pending'},
    {step:'report_write',label:'Report',status:'pending'},
  ];
  const latest=new Map();
  events.forEach(e=>{if(e&&e.step)latest.set(String(e.step),e)});
  return Array.from(latest.values()).map(e=>({step:e.step,label:String(e.step||'').replace(/_/g,' '),status:e.status||'running',message:e.message||''}));
}
function validationProgressPercent(detail={}){
  const explicit=Number(detail.progress||detail.percent||detail.summary?.progress);
  if(Number.isFinite(explicit)&&explicit>0)return Math.max(0,Math.min(100,explicit));
  const status=String(detail.status||detail.view?.header?.status||detail.state?.status||detail.summary?.status||'').toLowerCase();
  if(TERMINAL_STATUSES.has(status)||status.includes('success')||status.includes('succeed'))return 100;
  const items=validationTimelineItems(detail);
  const order=['bootstrap','dependencies','auth','live_wait','api_validation','generation_smoke','inference_gate','report_write','done'];
  let score=0;
  items.forEach(item=>{
    const idx=order.indexOf(String(item.step||''));
    const st=String(item.status||'').toLowerCase();
    if(idx>=0&&(st.includes('success')||st==='done'||st==='completed'))score=Math.max(score,idx+1);
    if(idx>=0&&(st.includes('running')||st.includes('started')||st.includes('waiting')||st.includes('pending')))score=Math.max(score,idx+0.35);
  });
  return Math.max(0,Math.min(98,Math.round((score/order.length)*100)));
}
function uniqueStrings(values=[]){const out=[];for(const value of values){const text=String(value||'').trim();if(text&&!out.includes(text))out.push(text)}return out}
function validationEndpointNames(detail={}){
  const smoke=detail.generation_smoke||detail.summary?.generation_smoke||detail.state?.generation_smoke||{};
  const schema=detail.api_schema||detail.tests?.api_schema||{};
  const names=[];
  for(const source of [schema.api_names,schema.discovered_api_names,smoke.discovered_api_names,smoke.api_names]){
    if(Array.isArray(source))names.push(...source);
  }
  if(smoke.api_name)names.push(smoke.api_name);
  if(schema.selected_api_name)names.push(schema.selected_api_name);
  const nested=schema.schema&&typeof schema.schema==='object'?schema.schema:{};
  for(const source of [nested.api_names,nested.named_endpoints]){
    if(Array.isArray(source))names.push(...source);
  }
  return uniqueStrings(names.map(normalizeApiName));
}
function endpointSchemaEntriesFromApiSchema(schema={}){
  const raw=schema.schema&&typeof schema.schema==='object'?schema.schema:schema;
  const named=raw.named_endpoints||raw.endpoints||raw.dependencies||[];
  if(Array.isArray(named))return named;
  if(named&&typeof named==='object')return Object.entries(named).map(([api_name,value])=>({api_name,...(value&&typeof value==='object'?value:{})}));
  return [];
}
function endpointNameFromEntry(entry={}){
  return normalizeApiName(entry.api_name||entry.api_name_for_predict||entry.name||entry.endpoint||entry.path||entry.id||'');
}
function validationEndpointParameters(detail={},endpointName=''){
  const smoke=detail.generation_smoke||detail.summary?.generation_smoke||detail.state?.generation_smoke||{};
  const schema=detail.api_schema||detail.tests?.api_schema||{};
  const selected=normalizeApiName(endpointName||smoke.api_name||schema.selected_api_name||formValue('validateApi')||'');
  const entries=endpointSchemaEntriesFromApiSchema(schema);
  const match=entries.find(entry=>endpointNameFromEntry(entry)===selected);
  const possible=match?.parameters||match?.inputs||match?.input_components||match?.components||smoke.endpoint_parameters||schema.endpoint_parameters||schema.schema?.endpoint_parameters||[];
  const params=Array.isArray(possible)?possible.map(item=>typeof item==='string'?item:(item?.parameter_name||item?.label||item?.name||item?.id||item?.component||'')):[];
  return uniqueStrings(params);
}
function applyValidationEndpoint(endpointName,detail={}){
  const name=normalizeApiName(endpointName);
  setInputValue('validateApi',name);
  const input=$('validateApi');
  if(input){input.dispatchEvent(new Event('input',{bubbles:true}));input.focus({preventScroll:true})}
  renderValidationEndpoints(detail||state.validationDetail||{});
  showMessage(`API name set to ${name}.`,'success');
}
function bindValidationEndpointPicker(root){
  if(!root||root.dataset.boundEndpointPicker==='true')return;
  root.dataset.boundEndpointPicker='true';
  root.addEventListener('click',e=>{
    const btn=e.target.closest('[data-endpoint-name]');
    if(!btn)return;
    e.preventDefault();
    applyValidationEndpoint(btn.dataset.endpointName,state.validationDetail||{});
  });
}
function endpointInputItemsFromEntry(entry={}){
  const raw=entry.parameters||entry.inputs||entry.input_components||entry.components||[];
  if(!Array.isArray(raw))return [];
  return raw.map((item,index)=>{
    if(typeof item==='string')return {name:item,type:'',component:''};
    const name=item.parameter_name||item.label||item.name||item.id||item.component||`arg${index}`;
    const type=item.type||item.python_type||item.datatype||item.api_info?.type||item.component_type||'';
    const component=item.component||item.component_type||item.type_name||item.label||'';
    const optional=item.optional===true||item.required===false;
    return {name:String(name||''),type:String(type||''),component:String(component||''),optional};
  }).filter(x=>x.name);
}
function endpointOutputItemsFromEntry(entry={}){
  const raw=entry.returns||entry.outputs||entry.output_components||[];
  if(!Array.isArray(raw))return [];
  return raw.map((item,index)=>{
    if(typeof item==='string')return {name:item,type:'',component:''};
    return {name:String(item.label||item.name||item.id||`output${index}`),type:String(item.type||item.python_type||item.datatype||''),component:String(item.component||item.component_type||item.type_name||'')};
  }).filter(x=>x.name);
}
function endpointDetails(detail={},endpointName=''){
  const schema=detail.api_schema||detail.tests?.api_schema||{};
  const entries=endpointSchemaEntriesFromApiSchema(schema);
  const selected=normalizeApiName(endpointName||formValue('validateApi')||detail.generation_smoke?.api_name||schema.selected_api_name||'');
  const match=entries.find(entry=>endpointNameFromEntry(entry)===selected)||{};
  const inputs=endpointInputItemsFromEntry(match);
  const fallbackParams=validationEndpointParameters(detail,selected).map(name=>({name,type:'',component:''}));
  return {
    name:selected,
    description:match.description||match.documentation||match.doc||'',
    inputs:inputs.length?inputs:fallbackParams,
    outputs:endpointOutputItemsFromEntry(match),
    raw:match,
  };
}
function renderEndpointParamChips(items=[]){
  if(!items.length)return '<span class="muted">No parameters discovered yet.</span>';
  return items.slice(0,10).map(item=>{
    const detail=[item.type,item.component,item.optional?'optional':''].filter(Boolean).join(' · ');
    return `<em title="${escapeHtml(detail||item.name)}"><strong>${escapeHtml(item.name)}</strong>${detail?`<span>${escapeHtml(detail)}</span>`:''}</em>`;
  }).join('')+(items.length>10?'<em><strong>…</strong></em>':'');
}
function renderValidationEndpoints(detail={}){
  const root=$('spaceTestEndpoints');
  if(!root)return;
  bindValidationEndpointPicker(root);
  const names=validationEndpointNames(detail);
  const selected=normalizeApiName(formValue('validateApi')||detail.generation_smoke?.api_name||detail.api_schema?.selected_api_name||names[0]||'');
  if(!names.length){root.innerHTML='<div class="empty compact-empty">No Gradio endpoints discovered yet. They will appear after the live API schema step.</div>';return}
  const chips=names.slice(0,12).map(name=>`<button class="endpoint-chip ${name===selected?'selected':''}" type="button" data-endpoint-name="${escapeHtml(name)}" title="Use ${escapeHtml(name)} for validation"><span>${escapeHtml(name)}</span>${name===selected?'<em>selected</em>':''}</button>`).join('');
  const details=endpointDetails(detail,selected);
  const signature=JSON.stringify({names:names.slice(0,12),selected,inputs:details.inputs,outputs:details.outputs,description:details.description||''});
  if(root.dataset.signature===signature)return;
  root.dataset.signature=signature;
  const inputLine=`<div class="endpoint-io"><strong>Inputs</strong><span>${renderEndpointParamChips(details.inputs)}</span></div>`;
  const outputLine=details.outputs.length?`<div class="endpoint-io"><strong>Outputs</strong><span>${renderEndpointParamChips(details.outputs)}</span></div>`:'';
  root.innerHTML=`<div class="endpoint-chip-list">${chips}</div><div class="endpoint-info-card">${inputLine}${outputLine}${details.description?`<p>${escapeHtml(details.description)}</p>`:''}</div>`;
}
function setGeneratedSpaceLinksEnabled(links={},enabled=false){
  const space=enabled?links.target_space_url:'';
  const settings=enabled?(links.target_space_settings_url||links.target_space_url&&links.target_space_url+'/settings'):'';
  for(const id of ['activeOpenSpace','openSpace','launchOpenSpace'])setLink(id,space);
  for(const id of ['activeOpenSettings','openSettings','manualOpenSettings','launchOpenSettings'])setLink(id,settings);
}
function setQuickLinks(links={},context={}){links=normalizeLinks(links||{});state.activeLinks=links;for(const id of ['activeOpenJob','openJob','launchOpenJob'])setLink(id,links.job_url);const runForLinks=links.run_id||context.run_id||context.summary?.run_id||state.runId;let spaceReady=runPassedSpaceCreation(context);if(spaceReady&&links.target_space_url)state.spaceLinksUnlockedFor=runForLinks;if(!spaceReady&&state.spaceLinksUnlockedFor&&state.spaceLinksUnlockedFor===runForLinks&&links.target_space_url)spaceReady=true;setGeneratedSpaceLinksEnabled(links,spaceReady);for(const id of ['activeOpenArtifacts','openArtifacts','launchOpenArtifacts'])setLink(id,links.artifacts_url);renderManualAction({links:{...links,target_space_url:spaceReady?links.target_space_url:'',target_space_settings_url:spaceReady?links.target_space_settings_url:''}})}
let operationTimer=null;function toastDuration(type='info'){if(type==='loading')return 12000;if(type==='error')return 7000;if(type==='warning'||type==='warn')return 6000;return 3500}
function setOperation(message,type='info',opts={}){const el=$('operationStatus');if(!el)return;if(operationTimer){clearTimeout(operationTimer);operationTimer=null}if(!message){el.hidden=true;el.textContent='';el.className='operation-status floating-toast';return}el.hidden=false;el.className='operation-status floating-toast '+type;el.textContent=message;const duration=opts.duration??toastDuration(type);if(duration>0)operationTimer=setTimeout(()=>{setOperation('')},duration)}
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}
function setBusy(isBusy,label='Working…'){state.busy=Boolean(isBusy);if(!state.busy&&$('operationStatus')?.className.includes('loading'))setOperation('');document.querySelectorAll('[data-action-button]').forEach(btn=>{btn.disabled=state.busy||btn.dataset.requiresBucket==='true'&&!state.bucketReady});if($('launchBuild'))setBuildLaunchEnabled();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;if(id==='launchBuild')setBuildLaunchEnabled();else btn.disabled=state.busy||btn.dataset.requiresBucket==='true'&&!state.bucketReady;btn.classList.remove('is-busy')}}
function setBucketFeedback(mode='checking',message='Checking private bucket access…'){
  const orb=$('bucketInlineOrb');
  if(orb)orb.className='bucket-status-orb '+mode;
  setText('bucketFeedbackText',message);
}
function setBucketReady(ok,message){state.bucketReady=Boolean(ok);setBuildLaunchEnabled();const feedback=state.bucketReady?'Ready for builds, validations and artifacts.':(message||'Create your private run bucket before launching builds.');setText('buildGateText',state.bucketReady?'Bucket ready. You can launch builds.':feedback);setText('bucketInlineStatus',state.bucketReady?'Ready':'Setup needed');setBucketFeedback(state.bucketReady?'ready':'error',feedback);for(const id of ['bucketInlineDot','sidebarBucketDot']){const dot=$(id);if(dot)dot.className='status-dot '+(state.bucketReady?'green':'amber-dot')}const inline=$('bucketInlineStatus');if(inline)inline.className='badge '+(state.bucketReady?'success':'warn');document.querySelectorAll('[data-requires-bucket="true"]').forEach(btn=>btn.disabled=state.busy||!state.bucketReady);setBuildLaunchEnabled()}
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}
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':''}`}

function normalizeModelInput(value){return String(value||'').trim().replace('https://huggingface.co/','').split('?')[0].split('#')[0].replace(/^\/+|\/+$/g,'')}
function modelScanMatchesCurrent(){return state.modelScan?.ok&&normalizeModelInput(state.modelScan.model_id)===normalizeModelInput(formValue('modelId'))}
function scanVerdictClass(verdict){verdict=String(verdict||'').toLowerCase();if(verdict==='safe')return'success';if(verdict==='caution')return'warn';if(verdict==='risky'||verdict==='unsupported')return'error';if(verdict==='scanning')return'running';return'neutral'}
function outputTypeFromModelScan(scan={}){
  const value=String(scan.expected_output_type||scan.metadata?.expected_output_type||'').toLowerCase();
  return ['text','image','video','audio','any'].includes(value)?value:'';
}
function syncExpectedOutputFromModelScan(scan={}){
  const inferred=outputTypeFromModelScan(scan);
  const select=$('buildExpectedOutput');
  if(!inferred||!select)return false;
  const previous=select.value;
  select.value=inferred;
  select.dataset.prescanOutput=inferred;
  select.dataset.prescanPreviousOutput=previous||'';
  select.dispatchEvent(new Event('change',{bubbles:true}));
  return previous!==inferred;
}
function renderModelPreScan(scan=null){
  const card=$('modelPreScanCard'),badge=$('modelPreScanBadge'),signals=$('modelPreScanSignals'),ackWrap=$('modelRiskAckWrap');
  if(!card)return;
  const current=normalizeModelInput(formValue('modelId'));
  const matches=scan&&scan.ok&&normalizeModelInput(scan.model_id)===current;
  if(!scan||!matches){
    card.className='model-prescan-card required';
    setText('modelPreScanTitle','Pre-scan required');
    setText('modelPreScanSummary','Run a fast metadata scan before launching a paid Job. It checks the model card guidance, runnable examples, Diffusers signals, files, safetensors, gated access and custom-code risk without downloading weights.');
    if(badge){badge.textContent='Required';badge.className='badge neutral'}
    if(signals){signals.hidden=true;signals.innerHTML=''}
    if(ackWrap)ackWrap.hidden=true;
    return;
  }
  const verdict=String(scan.verdict||'caution').toLowerCase();
  const cls=scanVerdictClass(verdict);
  card.className='model-prescan-card '+cls;
  setText('modelPreScanTitle',`${verdict.charAt(0).toUpperCase()+verdict.slice(1)} · score ${scan.score ?? '—'}/100`);
  setText('modelPreScanSummary',scan.summary||'Model metadata scan completed.');
  if(badge){badge.textContent=verdict;badge.className='badge '+cls}
  const good=(scan.good_signals||[]).slice(0,3);
  const risk=(scan.risk_signals||[]).slice(0,4);
  const outputType=outputTypeFromModelScan(scan);
  const recs=[...(scan.recommendations||[]).slice(0,2)];
  if(outputType)recs.unshift(`Expected output: ${outputType}`);
  if(signals){
    signals.hidden=false;
    signals.innerHTML=`${good.length?`<div><strong>Good</strong>${good.map(x=>`<span class="good">${escapeHtml(x)}</span>`).join('')}</div>`:''}${risk.length?`<div><strong>Watch</strong>${risk.map(x=>`<span class="risk">${escapeHtml(x)}</span>`).join('')}</div>`:''}${recs.length?`<div><strong>Recommendation</strong>${recs.map(x=>`<span>${escapeHtml(x)}</span>`).join('')}</div>`:''}`;
  }
  if(ackWrap)ackWrap.hidden=!(verdict==='risky');
}
function resetModelPreScan(){state.modelScan=null;const ack=$('modelRiskAck');if(ack)ack.checked=false;renderModelPreScan(null);setBuildLaunchEnabled()}
function setBuildLaunchEnabled(){const btn=$('launchBuild');const err=validateBuildForm();if(btn)btn.disabled=state.busy||Boolean(err);if($('buildGateText'))setText('buildGateText',err||'Bucket and model pre-scan ready. You can launch builds.')}
async function scanModel(){
  const model=formValue('modelId');
  if(!model){showMessage('Enter a model card URL or model ID before scanning.',true);return}
  setButtonBusy('scanModel',true,'Scanning…');
  const badge=$('modelPreScanBadge');if(badge){badge.textContent='Scanning';badge.className='badge running'}
  setText('modelPreScanTitle','Scanning model metadata…');
  setText('modelPreScanSummary','Reading the model card, Hub metadata and small config files. We do not download model weights.');
  try{
    const result=await apiPost('/api/models/pre-scan',{model_id:model});
    state.modelScan=result;
    syncExpectedOutputFromModelScan(result);
    const ack=$('modelRiskAck');if(ack)ack.checked=false;
    renderModelPreScan(result);
    setBuildLaunchEnabled();
    showMessage(`Model pre-scan: ${result.verdict} (${result.score}/100)`,result.verdict==='safe'?'success':(result.verdict==='unsupported'||result.verdict==='risky'?true:'warning'));
  }catch(e){state.modelScan=null;renderModelPreScan(null);showMessage(e.message,true)}
  finally{setButtonBusy('scanModel',false);setBuildLaunchEnabled()}
}

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('piModel'))return'Enter the Pi model to use.';if(!modelScanMatchesCurrent())return'Run the model pre-scan before launching this build.';const verdict=String(state.modelScan?.verdict||'').toLowerCase();if(verdict==='unsupported')return'This model pre-scan marked the repo as unsupported for automatic build.';if(verdict==='risky'&&!$('modelRiskAck')?.checked)return'Review the model pre-scan risk signals and confirm before launching.';return''}
function parseJsonField(id,fallback){const raw=formValue(id);if(!raw)return fallback;return JSON.parse(raw)}
function prettyJson(value){return JSON.stringify(value,null,2)}
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''}
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(){
  const cancel=$('cancelRun');if(cancel)cancel.disabled=true;
  if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;if(typeof resetProgressStability==='function')resetProgressStability(null);
  if(typeof activityFeedState!=='undefined'){activityFeedState.lastItems=[];activityFeedState.runId=null}
  for(const [id,value] of [
    ['detailRunId','—'],['detailJobStatus','—'],['detailGateStatus','—'],['detailSpace','—'],['detailHardware','—'],['detailTraces','—'],
    ['homeRunDetailStatus','No run'],['selectedModelId','—'],['progressTitle','No run selected'],['progressStatus','Idle'],
    ['currentStep','—'],['lastEvent','Select a run from the explorer or launch a new build.'],['elapsed','—'],['lastPolled','—'],
    ['progressPercent','0%']
  ])setText(id,value);
  const status=$('progressStatus');if(status)status.className='badge neutral';
  const fill=$('progressFill');if(fill){fill.style.width='0%';fill.classList.remove('is-running','progress-bump')}
  setQuickLinks({});
  const full=$('reportPreview');if(full)full.textContent='No report selected.';
  const manual=$('manualActionPanel');if(manual)manual.hidden=true;
  
  const activity=$('activityFeed');if(activity){activity.dataset.signature='';activity.classList.remove('show-all');activity.innerHTML='<div class="empty">No activity yet.</div>'}
  
  const events=$('eventsPanel');if(events)events.textContent='No events yet.';
  const artifacts=$('artifactList');if(artifacts)artifacts.textContent='No artifacts indexed yet.';
  const recovery=$('agentRecoveryCard');if(recovery){recovery.hidden=true;const body=$('agentRecoveryBody');if(body)body.innerHTML='';const badge=$('agentRecoveryBadge');if(badge){badge.textContent='Not needed';badge.className='badge neutral'}}
  const docs=$('runDocumentsDock');if(docs)docs.innerHTML='<div class="empty">Run documents will appear as files are written to the bucket.</div>';
  const blockers=$('selectedBlockers');if(blockers){blockers.hidden=true;blockers.innerHTML=''}
  for(const id of ['deleteActiveRun','activePrefillSpaceTest','deleteValidationRun']){const btn=$(id);if(btn)btn.disabled=true}
  renderTimeline(DEFAULT_TIMELINE,'idle');
} function prepareNewBuild(){if(state.poll){clearInterval(state.poll);state.poll=null}state.runId=null;state.activeRunProgress=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','—');if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;setText('lastPolled','—');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')}
function setActiveRun(result){if(state.poll){clearInterval(state.poll);state.poll=null}resetRunDetails();state.runId=result.run_id;state.spaceLinksUnlockedFor=null;if(Array.isArray(state.runsCache)&&typeof sortRunsNewestFirst==='function'){state.runsCache=sortRunsNewestFirst([{...result,run_id:result.run_id,status:result.status||'running',created_at:result.created_at||new Date().toISOString()},...state.runsCache.filter(r=>r.run_id!==result.run_id)]);renderRunsFromCache();}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||'—');renderActiveRunMeta({summary:result,state:result,launch:result});renderProgress({run_id:result.run_id,status:result.status||'running',progress:1,current_step_label:'Launching build Job…',events:[{step:'launch',status:'started',message:'Build Job launched; waiting for first worker event.'}],summary:result,state:result});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},result);saveActiveRun();startPolling(result.run_id)}
function startPolling(runId,options={}){
  if(!runId)return;
  const sameRun=state.runId===runId;
  if(sameRun&&state.poll)return;
  if(!sameRun){
    state.spaceLinksUnlockedFor=null;
    const blockers=$('selectedBlockers');if(blockers){blockers.hidden=true;blockers.innerHTML=''}
  }
  state.runId=runId;
  state.pollErrors=0;
  state.lastJobLogPoll=0;
  if(typeof resetProgressStability==='function')resetProgressStability(runId);
  if(state.poll)clearInterval(state.poll);
  saveActiveRun();
  if(!options.deferFirstTick)refreshProgress();
  else setTimeout(()=>{if(state.runId===runId)refreshProgress()},80);
  state.poll=setInterval(refreshProgress,2000);
}
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||{},resumable.view||{});startPolling(state.runId);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');setBucketFeedback('checking','Checking private bucket access…');try{const me=await apiGet('/api/me');state.user=me;setText('userChip','@'+me.username);const userChip=$('userChip');if(userChip)userChip.href=`https://huggingface.co/${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);renderBillingPanel(me);loadBillingStatus().catch(()=>{});await checkBucket(false);await loadRuns(true);clearSavedRun();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 userChip=$('userChip');if(userChip)userChip.href='/oauth/huggingface/login';const logout=$('logoutLink');if(logout)logout.hidden=true;renderSignedOut();renderBillingPanel(null);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>`}
function renderBillingPanel(info){
  // Open HF Billing: Live quotas, spend and compute usage remain in the HF Billing dashboard.
  const badge=$('billingStatusBadge');
  const canPay=info?.can_pay===true||info?.payment_ready===true;
  if(badge){
    badge.textContent=canPay?'Billing ready':'Check billing';
    badge.className='badge '+(canPay?'success':'warn');
  }
  const billing=$('openBilling');
  if(billing)billing.href=info?.links?.billing||'https://huggingface.co/settings/billing';
}
async function loadBillingStatus(){
  try{const data=await apiGet('/api/billing/status');renderBillingPanel({...state.user,...data});return data}catch(e){console.warn('billing status failed',e);return null}
}
async function checkBucket(noisy=true){setButtonBusy('checkBucket',true,'Checking…');setBucketFeedback('checking','Checking private bucket access…');try{const data=await apiGet(`/api/bucket/status?bucket_name=${encodeURIComponent(state.bucketName)}`);const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Create your private run bucket before launching builds.');if(noisy)showMessage(data.ok?`Bucket ready: ${source}`:`Bucket not ready: ${data.error||'not found'}`,!data.ok);return data}catch(e){setBucketReady(false,e.message);if(noisy)showMessage(e.message,true);throw e}finally{setButtonBusy('checkBucket',false)}}
async function createBucket(){setButtonBusy('createBucket',true,'Creating…');setBucketFeedback('creating','Creating private bucket…');try{const data=await apiPost('/api/bucket/create',{bucket_name:state.bucketName});const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Could not create bucket.');showMessage(data.ok?`Created/ready: ${source}`:`Could not create bucket: ${data.error}`,!data.ok);if(data.ok)loadRuns(true)}catch(e){setBucketReady(false,e.message);showMessage(e.message,true)}finally{setButtonBusy('createBucket',false)}}

function switchCenterTab(tab='active'){
  const next=tab==='validate'?'validate':'active';
  state.centerTab=next;
  document.querySelectorAll('[data-center-tab]').forEach(btn=>{
    const active=btn.dataset.centerTab===next;
    btn.classList.toggle('active',active);
    btn.setAttribute('aria-selected',active?'true':'false');
  });
  document.querySelectorAll('[data-center-panel]').forEach(panel=>{
    const active=panel.dataset.centerPanel===next;
    panel.classList.toggle('active',active);
    panel.hidden=!active;
  });
}
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 defaultValidationArgs(expected='text'){
  const kind=String(expected||'text').toLowerCase();
  if(kind==='image')return ["a cinematic robot cat astronaut, detailed, studio lighting"];
  if(kind==='video')return ["a short cinematic shot of a robot cat astronaut walking on the moon"];
  if(kind==='audio')return ["A calm voice saying hello from Agentic Space Factory."];
  return ["Explain the concept of agentic AI in one paragraph."];
}
function normalizeApiName(value){
  value=String(value||'').trim();
  if(!value)return'/generate';
  return value.startsWith('/')?value:'/'+value;
}
function firstPositiveNumber(...values){
  for(const value of values.flat()){
    const n=Number(value);
    if(Number.isFinite(n)&&n>0)return n;
  }
  return null;
}
function formatLatencySeconds(value){
  const n=Number(value);
  if(!Number.isFinite(n)||n<=0)return'—';
  if(n<10)return`${n.toFixed(2).replace(/\.00$/,'')}s`;
  if(n<60)return`${n.toFixed(1).replace(/\.0$/,'')}s`;
  return fmtSeconds(Math.round(n));
}
function parsedJsonOrNull(value){
  if(value==null||value==='')return null;
  try{return typeof value==='string'?JSON.parse(value):value}catch(_){return null}
}
function validationPrefillFromDetail(detail={}){
  const progress=(state.activeRunProgress&&((state.activeRunProgress.run_id||state.activeRunProgress.summary?.run_id)===(detail.run_id||detail.summary?.run_id||state.runId)))?state.activeRunProgress:{};
  const summary={...(detail.summary||{}),...(progress.summary||{})};
  const smoke={...(detail.state?.generation_smoke||{}),...(detail.tests?.generation_smoke||{}),...(detail.generation_smoke||{}),...(progress.generation_smoke||{}),...(progress.state?.generation_smoke||{})};
  const launch={...(detail.launch||{}),...(progress.launch||{})};
  const stateObj={...(detail.state||{}),...(progress.state||{})};
  const gate={...(detail.inference_gate||{}),...(progress.inference_gate||{})};
  const expected=smoke.expected_output_type||gate.expected_output_type||launch.expected_output_type||summary.expected_output_type||stateObj.expected_output_type||formValue('buildExpectedOutput')||'text';
  let args=Array.isArray(smoke.test_args)?smoke.test_args:parsedJsonOrNull(launch.test_args_json);
  let kwargs=smoke.test_kwargs&&typeof smoke.test_kwargs==='object'&&!Array.isArray(smoke.test_kwargs)?smoke.test_kwargs:parsedJsonOrNull(launch.test_kwargs_json);
  if(!Array.isArray(args))args=defaultValidationArgs(expected);
  if(!kwargs||Array.isArray(kwargs)||typeof kwargs!=='object')kwargs={};
  const target=summary.target_space||stateObj.target_space||launch.target_space||progress.target_space||$('detailSpace')?.textContent||'';
  const api=normalizeApiName(smoke.api_name||gate.api_name||launch.api_name||progress.api_name||'/generate');
  return {target,api,expected,args,kwargs};
}
function renderValidationStatus(data={}){
  state.validationDetail=data||state.validationDetail;
  const summary=data.summary||{};
  const smoke=data.generation_smoke||data.summary?.generation_smoke||data.state?.generation_smoke||{};
  const status=data.status||data.view?.header?.status||data.state?.status||summary.status||smoke.status||'running';
  const normalized=String(status||'running').toLowerCase();
  const klass=statusClass(status);
  const isRunning=!TERMINAL_STATUSES.has(normalized)&&klass!=='success'&&klass!=='error';
  const badge=$('spaceTestStatus');
  if(badge){badge.textContent=badgeLabel(status);badge.className='badge '+klass}
  const tabDot=$('spaceTestTabDot');
  if(tabDot)tabDot.className='tab-status-dot '+(isRunning?'running':klass);
  const card=$('spaceTestPreview');
  if(card)card.classList.toggle('is-validating',isRunning);
  const runId=data.run_id||summary.run_id||state.validationRunId||'—';
  setText('spaceTestOutput',runId);
  setText('lastPolled',formatTime(data.updated_at||summary.updated_at||data.state?.updated_at||data.created_at||summary.created_at||new Date().toISOString()));
  const latency=firstPositiveNumber(smoke.latency_seconds,smoke.observed_latency_seconds,summary.latency_seconds,summary.observed_latency_seconds,data.latency_seconds,data.observed_latency_seconds);
  setText('spaceTestLatency',latency?formatLatencySeconds(latency):'—');
  setText('spaceTestVerdict',badgeLabel(status));
  const deleteBtn=$('deleteValidationRun');
  if(deleteBtn){deleteBtn.disabled=!(runId&&runId!=='—');deleteBtn.onclick=()=>deleteRunById(runId,{kind:'validation',detail:data});}
  const percent=validationProgressPercent(data);
  setText('spaceTestProgressPercent',`${Math.round(percent)}%`);
  const fill=$('spaceTestProgressFill');
  if(fill){
    fill.style.width=`${Math.max(0,Math.min(100,percent))}%`;
    fill.className='validation-progress-fill '+(isRunning?'is-running':klass);
  }
  renderValidationTimeline(data);
  renderValidationEndpoints(data);
}
function renderValidationLaunchResult(result={}){
  state.validationRunId=result.run_id;
  renderValidationStatus({...result,status:result.status||'running'});
}
function renderValidationTimeline(detail={}){
  const root=$('spaceTestTimeline');
  if(!root)return;
  const items=validationTimelineItems(detail);
  root.innerHTML=items.map((item,idx)=>{
    const st=statusClass(item.status||'pending');
    const label=item.label||String(item.step||`Step ${idx+1}`).replace(/_/g,' ');
    return `<div class="validation-step ${st}"><span class="dot ${st}"></span><strong>${escapeHtml(label)}</strong><em>${escapeHtml(badgeLabel(item.status||'pending'))}</em></div>`;
  }).join('')||'<div class="empty">No validation events yet.</div>';
}
function showValidationRunDetail(detail={}){
  state.validationRunId=detail.run_id||detail.summary?.run_id||state.validationRunId;
  state.validationDetail=detail;
  stopValidationPolling();
  renderValidationStatus(detail);
  applyValidationPrefill(detail,'validation run');
  switchCenterTab('validate');
  const status=String(detail.view?.header?.status||detail.status||detail.state?.status||detail.summary?.status||'').toLowerCase();
  if(state.validationRunId&&!TERMINAL_STATUSES.has(status))startValidationPolling(state.validationRunId);
}
function stopValidationPolling(){
  if(state.validationPoll){clearInterval(state.validationPoll);state.validationPoll=null}
}
async function refreshValidationProgress(){
  if(!state.validationRunId)return;
  try{
    const p=await apiGet(`/api/runs/${encodeURIComponent(state.validationRunId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}&include_job_logs=1`);
    state.validationPollErrors=0;
    renderValidationStatus(p);
    const status=String(p.view?.header?.status||p.status||p.state?.status||'').toLowerCase();
    if(TERMINAL_STATUSES.has(status)){stopValidationPolling();showMessage(`Validation finished: ${badgeLabel(status)}`,statusClass(status)==='success'?'success':statusClass(status)==='error'?'error':'warning');loadRuns(false)}
  }catch(e){
    state.validationPollErrors+=1;
    console.warn('validation polling failed',e);
    if(state.validationPollErrors<=2)showMessage(`Validation polling failed. Retrying…`,'warning');
  }
}
function startValidationPolling(runId){
  state.validationRunId=runId;
  switchCenterTab('validate');
  stopValidationPolling();
  refreshValidationProgress();
  state.validationPoll=setInterval(refreshValidationProgress,3000);
}
function applyValidationPrefill(detail={},sourceLabel='selected Space'){
  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));
  switchCenterTab('validate');
  showMessage(`Prepared validation form for ${prefill.target||sourceLabel}.`,'info');
}
function prepareValidationFromActiveRun(){
  const selected=((state.selectedRunDetail?.run_id===state.runId||state.selectedRunDetail?.summary?.run_id===state.runId)?state.selectedRunDetail:{});
  const activeDetail={...selected,...(state.activeRunProgress||{}),summary:{...(selected.summary||{}),...(state.activeRunProgress?.summary||{})}};
  applyValidationPrefill(activeDetail,'active Space');
}
function prepareValidationFromSelectedRun(){
  applyValidationPrefill(state.selectedRunDetail||{},'selected Space');
}
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')||undefined,pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.checked??true,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 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)}}
async function refreshRunReportPreview(runId){
  if(!runId)return;
  try{
    const detail=await apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}`);
    if(detail.bucket_source)state.bucketSource=detail.bucket_source;
    state.selectedRunDetail=detail;
    if(typeof renderRunDetail==='function')renderRunDetail(detail);
    if(typeof renderActiveRunActions==='function')renderActiveRunActions(detail.summary||{},detail);
    if(typeof renderRunDocuments==='function')renderRunDocuments(detail);
    if(typeof renderMetrics==='function')renderMetrics({...detail,summary:detail.summary||{}});
    if(typeof renderBlockers==='function')renderBlockers(detail);
    if(typeof renderRunDetail!=='function'){
      const reportText=(detail.report||'No report available.').slice(0,8000);
      const report=$('reportPreview');
      if(report)report.textContent=reportText;
    }
  }catch(e){
    console.warn('report refresh failed',e);
  }
}
async function refreshProgress(){if(!state.runId||state.progressPollInFlight)return;state.progressPollInFlight=true;try{const now=Date.now();const includeLogs=!state.lastJobLogPoll||now-state.lastJobLogPoll>8000;if(includeLogs)state.lastJobLogPoll=now;const requestedRunId=state.runId;const p=await apiGet(`/api/runs/${encodeURIComponent(requestedRunId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}${includeLogs?'&include_job_logs=1':''}`);if(requestedRunId!==state.runId)return;if((p.run_id||p.summary?.run_id)&&state.runId&&(p.run_id||p.summary?.run_id)!==state.runId)return;state.activeRunProgress=p;state.pollErrors=0;if(p.bucket_source)state.bucketSource=p.bucket_source;renderProgress(p);renderEvents(p.events||[]);renderMetrics(p);if(typeof upsertRunExplorerFromProgress==='function')upsertRunExplorerFromProgress(p);if(typeof renderActiveRunActions==='function')renderActiveRunActions({...p.summary,bucket_source:p.bucket_source},p);if(typeof renderRunDocuments==='function')renderRunDocuments({...p,summary:{...p.summary,bucket_source:p.bucket_source}});if(typeof renderAgentRecovery==='function')renderAgentRecovery(p);if(typeof renderBlockers==='function')renderBlockers(p);if(p.view){setText('detailRunId',p.view.run_id||p.run_id);setText('selectedModelId',p.view.header?.model||p.summary?.model_id||'—')}else{setText('detailRunId',p.run_id||state.runId)}renderActiveRunMeta(p);if(p.state){setText('detailSpace',p.state.target_space||p.summary?.target_space||'—');const mode=runImplementationMode(p.summary||{},p);setText('detailImplementationMode',mode?implementationModeLabel(mode):'—');const modeEl=$('detailImplementationMode');if(modeEl&&mode)modeEl.title=implementationModeDescription(mode);setText('detailHardware',p.state.selected_hardware||p.summary?.selected_hardware||'—')}setText('detailJobStatus',p.status||p.state?.status||p.view?.header?.raw_status||'running');setText('detailGateStatus',p.inference_gate?.status||p.summary?.gate_status||'—');setText('detailTraces',Array.isArray(p.events)?String(p.events.length)+' events':'—');setText('homeRunDetailStatus',badgeLabel(p.status||p.state?.status||p.view?.header?.status||'running'));setQuickLinks({...p.links,...p.view?.links,bucket_source:p.bucket_source,run_id:p.run_id},{...p,events:p.events||[]});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;if(state.lastReportRefreshRunId!==state.runId){state.lastReportRefreshRunId=state.runId;setTimeout(()=>refreshRunReportPreview(state.runId),120);setTimeout(()=>loadRuns(true),160)}setOperation(`Run finished with status: ${badgeLabel(status)}`,status.includes('success')||status.includes('succeed')?'success':status.includes('failed')||status.includes('blocker')?'error':'warning')}if(now-(state.lastRunsRefreshFromPoll||0)>15000){state.lastRunsRefreshFromPoll=now;loadRuns(false)}}catch(e){state.pollErrors+=1;console.warn(e);setOperation(`Progress polling failed ${state.pollErrors} time${state.pollErrors===1?'':'s'}. Retrying automatically…`,'warning')}finally{state.progressPollInFlight=false}} function numberFromEvents(events=[],keys=[]){
  const rows=Array.isArray(events)?events:[];
  for(let i=rows.length-1;i>=0;i--){
    const event=rows[i]||{};
    const data=event.data||{};
    for(const key of keys){
      const direct=Number(event[key]);
      if(Number.isFinite(direct)&&direct>0)return direct;
      const nested=Number(data[key]);
      if(Number.isFinite(nested)&&nested>0)return nested;
    }
  }
  return null;
}
function readRunLatencyPayload(p={}){
  const smoke=p.generation_smoke||p.tests?.generation_smoke||p.summary?.generation_smoke||p.state?.generation_smoke||{};
  const gate=p.inference_gate||p.summary?.inference_gate||{};
  const summary=p.summary||{};
  const eventLatency=numberFromEvents(p.events||[],['latency_seconds','observed_latency_seconds']);
  const eventDuration=numberFromEvents(p.events||[],['recommended_zerogpu_duration_seconds','recommended_zero_gpu_duration_seconds','recommended_duration_seconds','zero_gpu_duration_recommendation']);
  const latency=firstPositiveNumber(smoke.latency_seconds,smoke.observed_latency_seconds,summary.latency_seconds,summary.observed_latency_seconds,p.latency_seconds,p.observed_latency_seconds,eventLatency);
  const duration=firstPositiveNumber(smoke.recommended_zerogpu_duration_seconds,smoke.recommended_zero_gpu_duration_seconds,smoke.recommended_duration_seconds,summary.recommended_zerogpu_duration_seconds,summary.recommended_zero_gpu_duration_seconds,summary.recommended_duration_seconds,gate.recommended_zerogpu_duration_seconds,gate.recommended_zero_gpu_duration_seconds,gate.zero_gpu_duration_recommendation,eventDuration);
  const source=smoke.recommendation_source||summary.recommendation_source||gate.recommendation_source||(eventLatency?'generation_smoke event':'');
  return {smoke,gate,latency,duration,source};
}
function renderActiveLatency(p={}){
  const card=$('activeLatencyCard');
  if(!card)return;
  const {latency,duration,source}=readRunLatencyPayload(p);
  const hasLatency=Number.isFinite(latency)&&latency>0;
  const hasDuration=Number.isFinite(duration)&&duration>0;
  setText('activeLatencyValue',hasLatency?formatLatencySeconds(latency):'—');
  setText('activeZeroGpuDuration',hasDuration?fmtSeconds(Math.round(duration)):'—');
  setText('activeLatencySource',hasLatency?(source||'live smoke test'):'measured when smoke test passes');
  const note=$('activeLatencyNote');
  if(note){
    note.textContent=hasDuration
      ? 'This measured latency is normally used to tune the `@spaces.GPU(duration=...)` decorator for ZeroGPU when the generated Space uses ZeroGPU.'
      : 'When available, the live smoke-test latency is used to recommend the `@spaces.GPU(duration=...)` value for ZeroGPU apps.';
  }
  card.classList.toggle('has-latency',hasLatency);
}
function runHardwareMeta(p={}){
  const summary=p.summary||{};
  const stateObj=p.state||{};
  const launch=p.launch||{};
  const hardware=p.hardware_strategy||summary.hardware_strategy||{};
  const target=summary.preferred_space_hardware||summary.target_gpu||stateObj.preferred_space_hardware||stateObj.target_gpu||launch.preferred_space_hardware||hardware.preferred||hardware.target||summary.selected_hardware||stateObj.selected_hardware||'—';
  const fallback=summary.fallback_space_hardware||summary.fallback_gpu||stateObj.fallback_space_hardware||stateObj.fallback_gpu||launch.fallback_space_hardware||hardware.fallback||'—';
  const output=summary.expected_output_type||stateObj.expected_output_type||launch.expected_output_type||p.generation_smoke?.expected_output_type||p.inference_gate?.expected_output_type||'—';
  return {target,fallback,output};
}
function renderActiveRunMeta(p={}){
  const meta=runHardwareMeta(p);
  setText('detailTargetGpu',meta.target||'—');
  setText('detailFallbackGpu',meta.fallback||'—');
  setText('detailExpectedOutput',meta.output||'—');
}
function renderMetrics(p){const root=$('metricsList');const {smoke,gate,latency,duration}=readRunLatencyPayload(p);const health=(gate.implementation_signals&&gate.implementation_signals.health_passed)||smoke.health_passed;const smokeOk=smoke.ok||smoke.status==='success';renderActiveLatency(p);if(!root)return;root.innerHTML=`<li><span>/health</span><strong class="${health?'metric-ok':''}">${health?'passed':'pending'}</strong></li><li><span>/generate smoke test</span><strong class="${smokeOk?'metric-ok':'metric-warn'}">${smokeOk?'passed':(smoke.status||'pending')}</strong></li><li><span>Latency</span><strong>${Number.isFinite(latency)&&latency>0?formatLatencySeconds(latency):'—'}</strong></li><li><span>Recommended ZeroGPU duration</span><strong>${Number.isFinite(duration)&&duration>0?fmtSeconds(Math.round(duration)):'—'}</strong></li>`}
function renderEvents(events){const root=$('eventsPanel');if(!root)return;try{const rows=Array.isArray(events)?events:[];root.textContent=rows.slice(-12).map(e=>`${e?.ts||''}  ${e?.step||''}  ${e?.status||''}  ${e?.message||''}`).join('\n')||'No events yet. The run may still be scheduling.'}catch(e){console.warn('renderEvents failed',e);root.textContent='Events could not be rendered.'}}
async function cancelActiveRun(){
  if(!state.runId){showMessage('Select a running run before cancelling.', 'warning');return}
  if(!confirm(`Cancel Job for run ${state.runId}?`))return;
  setButtonBusy('cancelRun',true,'Cancelling…');
  try{
    const result=await apiPost(`/api/runs/${encodeURIComponent(state.runId)}/cancel?bucket_name=${encodeURIComponent(state.bucketName)}`,{});
    showMessage(`Cancelled Job ${result.job_id||''}`,'success');
    if(state.poll){clearInterval(state.poll);state.poll=null}
    await refreshProgress();
    await loadRuns(true);
  }catch(e){
    showMessage(e.message,true);
  }finally{
    setButtonBusy('cancelRun',false);
  }
}
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('');if(view==='progress')loadRuns(false);if(view==='validate')switchCenterTab('validate');if(view==='active')switchCenterTab('active');const targetMap={build:'newBuildCard',progress:'runsExplorerPanel',validate:'spaceTestSection',active:'activeRunPanel'};const target=$(targetMap[view]||'');if(target)target.scrollIntoView({behavior:'smooth',block:'start'})}
function bindNavigation(){
  bindAllOnce('.nav-item','click','navItem',e=>switchView(e.currentTarget.dataset.view||'build'));
  bindAllOnce('[data-view-target]','click','viewTarget',e=>switchView(e.currentTarget.dataset.viewTarget||'build'));
  bindAllOnce('[data-center-tab]','click','centerTab',e=>switchCenterTab(e.currentTarget.dataset.centerTab||'active'));
}
function bindRunFilters(){
  bindAllOnce('.filter[data-status]','click','runStatusFilter',e=>{
    state.runStatusFilter=e.currentTarget.dataset.status||'all';
    state.runPage=0;
    renderRunsFromCache();
  });
  for(const id of ['runSearch','runSearchFull'])bindOnce($(id),'input',`runSearch${id}`,e=>{
    state.runSearch=e.target.value||'';
    state.runPage=0;
    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');
  const actions={
    launchBuild:launchBuild,
    scanModel:scanModel,
    launchValidate:launchValidate,
    prepareNewBuild:prepareNewBuild,
    manualGoValidate:prepareValidationFromActiveRun,
    activePrefillSpaceTest:prepareValidationFromActiveRun,
    checkBucket:()=>checkBucket(true),
    createBucket:createBucket,
    refreshRuns:()=>loadRuns(true),
    refreshRunsHome:()=>loadRuns(true),
    cancelRun:cancelActiveRun,
    deleteActiveRun:()=>deleteRunById(state.runId,{kind:'active',detail:state.selectedRunDetail||state.activeRunProgress||{}}),
    deleteValidationRun:()=>deleteRunById(state.validationRunId,{kind:'validation',detail:state.validationDetail||{}}),
  };
  Object.entries(actions).forEach(([id,handler])=>bindOnce($(id),'click',`action${id}`,handler));
  bindOnce($('modelId'),'input','modelPreScanReset',resetModelPreScan);
  bindOnce($('modelRiskAck'),'change','modelRiskAck',setBuildLaunchEnabled);
  ['tryZeroGpu','preferredHardware','fallbackHardware','allowFallback'].forEach(id=>bindOnce($(id),'change',`hardware${id}`,()=>setText('hardwarePlan',hardwarePlanLabel())));
  bindOnce($('implementationMode'),'change','implementationModeHelp',updateImplementationModeHelp);
}
bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());updateImplementationModeHelp();setBuildModeNew();renderTimeline(DEFAULT_TIMELINE);loadAppInfo();

// Pass 47 intentionally removed automatic switchView('progress') after launch and run selection.
// Compatibility note for v108 regression: implementationMode')?.addEventListener('change',updateImplementationModeHelp is now bound through bindOnce().

// Live quotas, spend and compute usage remain in the HF Billing dashboard; Open HF Billing for numeric values.

// Legacy endpoint picker wording kept for tests: API name used for validation / Set API name.