fffiloni commited on
Commit
c843485
Β·
verified Β·
1 Parent(s): c40901b

Upload 6 files

Browse files
web/static/app.css CHANGED
@@ -114,8 +114,8 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
114
 
115
 
116
  .active-run-panel{display:grid;gap:12px}
117
- .active-meta{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:12px 0}
118
- .active-meta div:nth-child(4),.active-meta div:nth-child(5){grid-column:auto}
119
  .summary-meta span,.detail-grid span,.validation-grid span{display:block;font-size:11px;color:var(--muted);margin-bottom:4px}
120
  .summary-meta strong,.detail-grid strong,.validation-grid strong{font-size:12px;line-height:1.25}
121
  .progress-caption{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:#344054;margin-top:2px}
@@ -485,6 +485,11 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
485
  .timeline .step{min-width:90px}
486
  }
487
 
 
 
 
 
 
488
  .cost-note{margin:6px 0 2px;font-size:11px;line-height:1.35;color:#7c2d12;background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;padding:7px 9px}
489
 
490
  /* v76: coherent status color system */
@@ -937,3 +942,138 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
937
  .validation-step strong{font-size:10px;line-height:1.15;text-transform:none;max-width:82px;white-space:normal;color:#344054}
938
  .validation-step em{font-size:9px;line-height:1;color:#667085;max-width:82px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
939
  .validation-step.success strong{color:#166534}.validation-step.error strong{color:#be123c}.validation-step.running strong{color:#1d4ed8}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
 
116
  .active-run-panel{display:grid;gap:12px}
117
+ .active-meta{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:12px 0}
118
+ .active-meta div{min-width:0}.active-meta strong{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.active-meta div:nth-child(4),.active-meta div:nth-child(5),.active-meta div:nth-child(6){grid-column:auto}
119
  .summary-meta span,.detail-grid span,.validation-grid span{display:block;font-size:11px;color:var(--muted);margin-bottom:4px}
120
  .summary-meta strong,.detail-grid strong,.validation-grid strong{font-size:12px;line-height:1.25}
121
  .progress-caption{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:#344054;margin-top:2px}
 
485
  .timeline .step{min-width:90px}
486
  }
487
 
488
+
489
+ .implementation-mode-help{margin:-4px 0 0;border:1px solid #dbeafe;background:#eff6ff;color:#1e3a8a;border-radius:10px;padding:8px 10px;font-size:11.5px;line-height:1.35}
490
+ .implementation-mode-help strong{display:block;font-size:12px;margin-bottom:2px;color:#172554}
491
+ .implementation-mode-help span{display:block}
492
+
493
  .cost-note{margin:6px 0 2px;font-size:11px;line-height:1.35;color:#7c2d12;background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;padding:7px 9px}
494
 
495
  /* v76: coherent status color system */
 
942
  .validation-step strong{font-size:10px;line-height:1.15;text-transform:none;max-width:82px;white-space:normal;color:#344054}
943
  .validation-step em{font-size:9px;line-height:1;color:#667085;max-width:82px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
944
  .validation-step.success strong{color:#166534}.validation-step.error strong{color:#be123c}.validation-step.running strong{color:#1d4ed8}
945
+
946
+ /* v104: robust run cards, endpoint picker, event feed and Active Run latency */
947
+ .run-row.run-card{
948
+ display:grid;
949
+ grid-template-columns:minmax(0,1fr);
950
+ gap:10px;
951
+ padding:12px;
952
+ border-bottom:1px solid var(--line);
953
+ overflow:hidden;
954
+ }
955
+ .run-row.run-card *{box-sizing:border-box;min-width:0}
956
+ .run-card-main{display:grid;grid-template-columns:10px minmax(0,1fr);gap:10px;align-items:start;width:100%;min-width:0}
957
+ .run-card-copy{display:grid;gap:7px;min-width:0;overflow:hidden}
958
+ .run-card-title{display:flex;align-items:center;gap:7px;min-width:0;max-width:100%}
959
+ .run-card-title .run-id{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;line-height:1.2;color:#101828}
960
+ .kind-chip{flex:0 0 auto;font-style:normal;font-size:10px;font-weight:850;border-radius:999px;padding:3px 7px;border:1px solid #dbe4f0;background:#fff;color:#344054;line-height:1}
961
+ .kind-chip.validation{border-color:#bfdbfe;background:#eff6ff;color:#1d4ed8}
962
+ .kind-chip.build{border-color:#e5e7eb;background:#f8fafc;color:#475467}
963
+ .run-card-meta{display:grid;grid-template-columns:1fr;gap:4px;width:100%;min-width:0;overflow:hidden}
964
+ .run-card-meta span{display:grid;grid-template-columns:auto minmax(0,1fr);gap:5px;align-items:baseline;width:100%;min-width:0;max-width:100%;font-size:11px;color:#667085;line-height:1.2;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
965
+ .run-card-meta span b{font-size:10px;letter-spacing:.02em;text-transform:uppercase;color:#98a2b3;font-weight:850;flex:0 0 auto}
966
+ .run-card-side{display:flex;width:100%;align-items:center;justify-content:space-between;gap:8px;min-width:0}
967
+ .run-card-side .pill{display:block;min-width:0;max-width:calc(100% - 42px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:0}
968
+ .run-card-side .icon-btn{width:34px;height:28px;border-radius:999px;display:grid;place-items:center;padding:0;font-size:16px;line-height:1}
969
+
970
+ .validation-endpoints-card{background:linear-gradient(180deg,#fff 0%,#f8fbff 100%)}
971
+ .validation-endpoints-card .card-title-row{gap:8px;align-items:center}
972
+ .endpoint-chip-list{gap:8px;margin-bottom:10px}
973
+ .endpoint-chip{cursor:pointer;padding:7px 10px;box-shadow:0 1px 2px rgba(16,24,40,.03)}
974
+ .endpoint-chip em{margin-left:2px;background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe}
975
+ .endpoint-chip.selected em{background:#fff;color:#1d4ed8}
976
+ .endpoint-selected-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:8px;align-items:center;margin-top:8px;padding:8px;border:1px solid #dbeafe;border-radius:10px;background:#eff6ff}
977
+ .endpoint-selected-row span{font-size:10px;text-transform:uppercase;letter-spacing:.04em;font-weight:850;color:#64748b}
978
+ .endpoint-selected-row strong{font-size:12px;color:#1d4ed8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
979
+ .endpoint-params{margin:8px 0 0;padding:8px;border-radius:10px;background:#fff;border:1px solid #eef2f7;color:#667085;font-size:11px;line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
980
+
981
+ .active-latency-card{background:linear-gradient(180deg,#fff 0%,#f8fafc 100%)}
982
+ .active-latency-card.has-latency{border-color:#bfdbfe;background:linear-gradient(180deg,#eff6ff 0%,#fff 100%)}
983
+ .active-latency-card .card-title-row .muted{font-size:10.5px}
984
+ .active-latency-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}
985
+ .active-latency-grid div{border:1px solid var(--line);border-radius:12px;background:#fff;padding:9px 10px;min-width:0}
986
+ .active-latency-grid span{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:#98a2b3;font-weight:850}
987
+ .active-latency-grid strong{display:block;margin-top:2px;font-size:15px;color:#101828;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
988
+ .latency-note{margin:8px 0 0;color:#667085;font-size:11px;line-height:1.35}
989
+
990
+ .activity-feed{max-height:300px;overflow:auto;scrollbar-width:thin}
991
+ .activity-row{grid-template-columns:62px 28px minmax(0,1fr) auto;align-items:start;padding:10px 0;gap:10px}
992
+ .activity-row time{font-size:11px;white-space:nowrap;color:#667085;padding-top:4px}
993
+ .activity-icon{width:24px;height:24px;border-radius:999px;display:grid;place-items:center;background:#f8fafc;border:1px solid #eef2f7;font-size:12px;box-shadow:0 1px 2px rgba(16,24,40,.04)}
994
+ .activity-row.success .activity-icon{background:#ecfdf3;border-color:#bbf7d0;color:#166534}
995
+ .activity-row.running .activity-icon{background:#eff6ff;border-color:#bfdbfe;color:#1d4ed8}
996
+ .activity-row.warning .activity-icon,.activity-row.warn .activity-icon{background:#fffbeb;border-color:#fde68a;color:#92400e}
997
+ .activity-row.error .activity-icon{background:#fff1f2;border-color:#fecdd3;color:#be123c}
998
+ .activity-row.hardware-fallback .activity-icon{background:#eff6ff;border-color:#bfdbfe;color:#1d4ed8}
999
+ .activity-copy{min-width:0;overflow:hidden}
1000
+ .activity-copy strong{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:12.5px;line-height:1.3;color:#101828;word-break:break-word}
1001
+ .activity-copy p{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;font-size:11px;line-height:1.3}
1002
+ .activity-copy .event-reason{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;white-space:normal;word-break:break-word}
1003
+ .activity-row>em{justify-self:end;max-width:92px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:2px}
1004
+ @media (max-width:720px){.activity-row{grid-template-columns:54px 26px minmax(0,1fr)}.activity-row>em{display:none}.active-latency-grid{grid-template-columns:1fr}.endpoint-selected-row{grid-template-columns:1fr}.endpoint-selected-row .secondary{width:100%}}
1005
+
1006
+ /* v105: UI cleanup, narrow cards and lighter event rendering */
1007
+ .run-row.run-card{
1008
+ border:1px solid var(--line);
1009
+ border-radius:14px;
1010
+ margin:8px 0;
1011
+ background:#fff;
1012
+ box-shadow:0 1px 2px rgba(16,24,40,.03);
1013
+ }
1014
+ .run-row.run-card + .run-row.run-card{border-top:1px solid var(--line)}
1015
+ .run-card-title{display:grid;grid-template-columns:minmax(0,1fr) auto;width:100%}
1016
+ .run-card-meta span{grid-template-columns:minmax(46px,max-content) minmax(0,1fr)}
1017
+ .run-card-meta span b::after{content:":"}
1018
+ .run-card-meta span em{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-style:normal;color:#667085}
1019
+ .run-card-side{padding-left:20px}
1020
+ .run-card-side .pill{max-width:100%;font-size:10.5px;text-align:center}
1021
+ .run-card-side .icon-btn{background:#f8fafc;border-color:#e5e7eb;color:#667085}
1022
+ .run-card-side .icon-btn:hover{background:#fff1f2;border-color:#fecdd3;color:#be123c}
1023
+ @media (max-width:420px){
1024
+ .run-row.run-card{padding:10px}
1025
+ .run-card-main{grid-template-columns:8px minmax(0,1fr);gap:8px}
1026
+ .run-card-title{grid-template-columns:minmax(0,1fr)}
1027
+ .kind-chip{justify-self:start}
1028
+ .run-card-side{padding-left:16px;display:grid;grid-template-columns:minmax(0,1fr) 34px}
1029
+ }
1030
+
1031
+ .endpoint-params{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;-webkit-line-clamp:unset}
1032
+ .endpoint-params>strong{font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:#64748b}
1033
+ .endpoint-params>span{display:flex;flex-wrap:wrap;gap:5px;min-width:0}
1034
+ .endpoint-params>span em{font-style:normal;font-size:10.5px;border:1px solid #e5e7eb;background:#f8fafc;border-radius:999px;padding:2px 6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#475467}
1035
+
1036
+ .activity-feed{max-height:320px;overscroll-behavior:contain}
1037
+ .activity-row{border-bottom:1px solid #eef2f7}
1038
+ .activity-row:last-child{border-bottom:0}
1039
+ .activity-copy p{display:flex;gap:6px;align-items:center;min-width:0;white-space:nowrap}
1040
+ .activity-copy p span,.activity-copy p em{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-style:normal}
1041
+ .activity-copy p span{max-width:55%;color:#667085}
1042
+ .activity-copy p em{color:#98a2b3}
1043
+
1044
+ /* v110: model pre-scan gate */
1045
+ .model-prescan-card{border:1px solid #e5e7eb;background:#fff;border-radius:14px;padding:11px;display:grid;gap:9px;box-shadow:0 1px 2px rgba(16,24,40,.03)}
1046
+ .model-prescan-card.required{background:#f8fafc}
1047
+ .model-prescan-card.success{border-color:#bbf7d0;background:#f0fdf4}
1048
+ .model-prescan-card.warn{border-color:#fed7aa;background:#fff7ed}
1049
+ .model-prescan-card.error{border-color:#fecdd3;background:#fff1f2}
1050
+ .model-prescan-card.running{border-color:#bfdbfe;background:#eff6ff}
1051
+ .model-prescan-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;min-width:0}
1052
+ .model-prescan-head>div{min-width:0;display:grid;gap:2px}.model-prescan-head strong{font-size:13px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mini-kicker{font-size:10px;text-transform:uppercase;letter-spacing:.08em;font-weight:800;color:#64748b}.model-prescan-card p{margin:0;font-size:12px;line-height:1.35}.model-prescan-signals{display:grid;gap:8px}.model-prescan-signals>div{display:grid;gap:5px}.model-prescan-signals strong{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:#64748b}.model-prescan-signals span{display:block;border:1px solid #e5e7eb;background:rgba(255,255,255,.72);border-radius:9px;padding:5px 7px;font-size:11.5px;line-height:1.3;color:#344054}.model-prescan-signals span.good{border-color:#bbf7d0}.model-prescan-signals span.risk{border-color:#fed7aa}.model-risk-ack{display:flex!important;align-items:flex-start;gap:8px;border:1px dashed #fb7185;background:#fff;padding:8px;border-radius:10px;font-size:12px;color:#9f1239}.model-risk-ack input{margin-top:2px}.model-risk-ack[hidden]{display:none!important}#scanModel{justify-self:start}
1053
+
1054
+ /* v112: final cohesion pass for production preview */
1055
+ .implementation-mode-help{
1056
+ display:grid;
1057
+ gap:2px;
1058
+ border:1px solid #eef2f7;
1059
+ border-radius:10px;
1060
+ background:#f8fafc;
1061
+ padding:8px 10px;
1062
+ font-size:11.5px;
1063
+ line-height:1.35;
1064
+ color:#667085;
1065
+ }
1066
+ .implementation-mode-help strong{font-size:11.5px;color:#344054}
1067
+ .model-prescan-card,.bucket-status-card,.run-stats-card{min-width:0;overflow:hidden}
1068
+ .model-prescan-card p,.model-prescan-signals span{word-break:break-word}
1069
+ .runs-table{display:grid;gap:8px}
1070
+ .run-row.run-card{margin:0}
1071
+ .run-card-meta span em{word-break:break-word}
1072
+ .activity-feed,.validation-timeline{overscroll-behavior:contain}
1073
+ .endpoint-chip{min-width:0;overflow:hidden}
1074
+ .endpoint-chip span,.endpoint-chip strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1075
+ @media (max-width:560px){
1076
+ .model-prescan-head{display:grid;grid-template-columns:1fr;gap:6px}
1077
+ .model-prescan-head .badge{justify-self:start}
1078
+ .run-stats-card{padding:10px}
1079
+ }
web/static/app.js CHANGED
@@ -1,6 +1,26 @@
1
- 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'};
2
  const TERMINAL_STATUSES=new Set(['done','success','failed','full_inference_success','full_inference_candidate_health_passed','manual_hardware_required','generated_needs_manual_hardware','technical_blocker','health_only','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled']);
3
  const DEFAULT_TIMELINE=[{label:'Bootstrap',status:'pending'},{label:'Dependencies',status:'pending'},{label:'Auth',status:'pending'},{label:'Model analysis',status:'pending'},{label:'Workspace',status:'pending'},{label:'Node/npm',status:'pending'},{label:'Pi install',status:'pending'},{label:'Pi config',status:'pending'},{label:'Pi run',status:'pending'},{label:'Pi verification',status:'pending'},{label:'Metadata sanitize',status:'pending'},{label:'Requirements sanitize',status:'pending'},{label:'Hardware strategy',status:'pending'},{label:'Create with hardware',status:'pending'},{label:'Create Space',status:'pending'},{label:'Repair pass',status:'pending'},{label:'Upload files',status:'pending'},{label:'Space runtime',status:'pending'},{label:'Space logs',status:'pending'},{label:'API validation',status:'pending'},{label:'Live wait',status:'pending'},{label:'Generation smoke',status:'pending'},{label:'Inference gate',status:'pending'},{label:'Report',status:'pending'},{label:'Done',status:'pending'},{label:'Failure',status:'pending'}];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  function saveActiveRun(){try{localStorage.setItem('asf.activeRun',JSON.stringify({runId:state.runId,bucketName:state.bucketName,links:state.activeLinks||{}}))}catch(_){}}
5
  function clearSavedRun(){try{localStorage.removeItem('asf.activeRun')}catch(_){} state.spaceLinksUnlockedFor=null}
6
  function artifactUrl(runId,bucketSource){if(!runId||!bucketSource)return'';return `https://huggingface.co/buckets/${bucketSource}/tree/runs/${runId}`}
@@ -72,23 +92,57 @@ function validationEndpointNames(detail={}){
72
  }
73
  return uniqueStrings(names.map(normalizeApiName));
74
  }
75
- function validationEndpointParameters(detail={}){
 
 
 
 
 
 
 
 
 
 
76
  const smoke=detail.generation_smoke||detail.summary?.generation_smoke||detail.state?.generation_smoke||{};
77
  const schema=detail.api_schema||detail.tests?.api_schema||{};
78
- const params=smoke.endpoint_parameters||schema.endpoint_parameters||schema.schema?.endpoint_parameters||[];
79
- return Array.isArray(params)?uniqueStrings(params):[];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  }
81
  function renderValidationEndpoints(detail={}){
82
  const root=$('spaceTestEndpoints');
83
  if(!root)return;
 
84
  const names=validationEndpointNames(detail);
85
  const selected=normalizeApiName(formValue('validateApi')||detail.generation_smoke?.api_name||detail.api_schema?.selected_api_name||'');
86
- const params=validationEndpointParameters(detail);
87
  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}
88
- const chips=names.slice(0,12).map(name=>`<button class="endpoint-chip ${name===selected?'selected':''}" type="button" data-endpoint-name="${escapeHtml(name)}"><span>${escapeHtml(name)}</span>${name===selected?'<em>selected</em>':''}</button>`).join('');
89
- const paramLine=params.length?`<p class="endpoint-params"><strong>Selected params:</strong> ${escapeHtml(params.slice(0,8).join(', '))}${params.length>8?'…':''}</p>`:'';
90
- root.innerHTML=`<div class="endpoint-chip-list">${chips}</div>${paramLine}`;
91
- root.querySelectorAll('[data-endpoint-name]').forEach(btn=>btn.addEventListener('click',()=>{setInputValue('validateApi',btn.dataset.endpointName);renderValidationEndpoints(detail)}));
 
92
  }
93
  function setGeneratedSpaceLinksEnabled(links={},enabled=false){
94
  const space=enabled?links.target_space_url:'';
@@ -100,17 +154,70 @@ function setQuickLinks(links={},context={}){links=normalizeLinks(links||{});stat
100
  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}
101
  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)}
102
  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}
103
- 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'))$('launchBuild').disabled=state.busy||!state.bucketReady;if($('launchValidate'))$('launchValidate').disabled=state.busy;if(state.busy)setOperation(label,'loading')}
104
- 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')}}
105
  function setBucketFeedback(mode='checking',message='Checking private bucket access…'){
106
  const orb=$('bucketInlineOrb');
107
  if(orb)orb.className='bucket-status-orb '+mode;
108
  setText('bucketFeedbackText',message);
109
  }
110
- function setBucketReady(ok,message){state.bucketReady=Boolean(ok);const btn=$('launchBuild');if(btn)btn.disabled=state.busy||!state.bucketReady;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)}
111
  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}
112
  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':''}`}
113
- 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''}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  function parseJsonField(id,fallback){const raw=formValue(id);if(!raw)return fallback;return JSON.parse(raw)}
115
  function prettyJson(value){return JSON.stringify(value,null,2)}
116
  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''}
@@ -207,6 +314,20 @@ function normalizeApiName(value){
207
  if(!value)return'/generate';
208
  return value.startsWith('/')?value:'/'+value;
209
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  function parsedJsonOrNull(value){
211
  if(value==null||value==='')return null;
212
  try{return typeof value==='string'?JSON.parse(value):value}catch(_){return null}
@@ -246,8 +367,8 @@ function renderValidationStatus(data={}){
246
  const runId=data.run_id||summary.run_id||state.validationRunId||'β€”';
247
  setText('spaceTestOutput',runId);
248
  setText('lastPolled',formatTime(data.updated_at||summary.updated_at||data.state?.updated_at||data.created_at||summary.created_at||new Date().toISOString()));
249
- const latency=smoke.latency_seconds||smoke.observed_latency_seconds||summary.latency_seconds||summary.observed_latency_seconds||data.latency_seconds||data.observed_latency_seconds;
250
- setText('spaceTestLatency',latency?fmtSeconds(Math.round(Number(latency))):'β€”');
251
  setText('spaceTestVerdict',badgeLabel(status));
252
  const deleteBtn=$('deleteValidationRun');
253
  if(deleteBtn){deleteBtn.disabled=!(runId&&runId!=='β€”');deleteBtn.onclick=()=>deleteRunById(runId,{kind:'validation',detail:data});}
@@ -346,7 +467,33 @@ async function refreshRunReportPreview(runId){
346
  console.warn('report refresh failed',e);
347
  }
348
  }
349
- 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 p=await apiGet(`/api/runs/${encodeURIComponent(state.runId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}${includeLogs?'&include_job_logs=1':''}`);state.activeRunProgress=p;state.pollErrors=0;if(p.bucket_source)state.bucketSource=p.bucket_source;renderProgress(p);renderEvents(p.events||[]);renderMetrics(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)}if(p.state){setText('detailSpace',p.state.target_space||p.summary?.target_space||'β€”');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;refreshRunReportPreview(state.runId)}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 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>`}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  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.'}}
351
  async function cancelActiveRun(){
352
  if(!state.runId){showMessage('Select a running run before cancelling.', 'warning');return}
@@ -366,9 +513,53 @@ async function cancelActiveRun(){
366
  }
367
  async function resumeLatestRun(){setButtonBusy('resumeLatestRun',true,'Resuming…');try{await loadRuns(true);const data=await apiGet(`/api/runs/resumable?bucket_name=${encodeURIComponent(state.bucketName)}`);if(!data.run){showMessage('No latest run found in this bucket.','warning');return}state.runId=data.run.run_id;if(data.bucket_source)state.bucketSource=data.bucket_source;setText('detailRunId',state.runId);setBuildModeInspecting(state.runId);setQuickLinks(data.view?.links||{},data.view||{});if(data.view)renderRunViewModel(data.view,{run_id:state.runId,progress:data.view.header?.status==='succeeded'?100:0,events:data.view.activity||[]});startPolling(state.runId);showMessage(`Resumed ${state.runId}`,'success')}catch(e){showMessage(e.message,true)}finally{setButtonBusy('resumeLatestRun',false)}}
368
  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'})}
369
- 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')));document.querySelectorAll('[data-center-tab]').forEach(btn=>btn.addEventListener('click',()=>switchCenterTab(btn.dataset.centerTab||'active')))}
370
- function bindRunFilters(){document.querySelectorAll('.filter[data-status]').forEach(btn=>btn.addEventListener('click',()=>{state.runStatusFilter=btn.dataset.status||'all';state.runPage=0;renderRunsFromCache()}));for(const id of ['runSearch','runSearchFull'])$(id)?.addEventListener('input',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)})}
371
- 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);$('activePrefillSpaceTest')?.addEventListener('click',prepareValidationFromActiveRun);$('deleteActiveRun')?.addEventListener('click',()=>deleteRunById(state.runId,{kind:'active',detail:state.selectedRunDetail||state.activeRunProgress||{}}));$('deleteValidationRun')?.addEventListener('click',()=>deleteRunById(state.validationRunId,{kind:'validation',detail:state.validationDetail||{}}));$('checkBucket')?.addEventListener('click',()=>checkBucket(true));$('createBucket')?.addEventListener('click',createBucket);$('refreshRuns')?.addEventListener('click',()=>loadRuns(true));$('refreshRunsHome')?.addEventListener('click',()=>loadRuns(true));$('resumeLatestRun')?.addEventListener('click',resumeLatestRun);$('cancelRun')?.addEventListener('click',cancelActiveRun);$('resumeLatestRunFull')?.addEventListener('click',resumeLatestRun);['tryZeroGpu','preferredHardware','fallbackHardware','allowFallback'].forEach(id=>$(id)?.addEventListener('change',()=>setText('hardwarePlan',hardwarePlanLabel())))}
372
- bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());setBuildModeNew();renderTimeline(DEFAULT_TIMELINE);loadAppInfo();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
  // Pass 47 intentionally removed automatic switchView('progress') after launch and run selection.
 
 
1
+ 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};
2
  const TERMINAL_STATUSES=new Set(['done','success','failed','full_inference_success','full_inference_candidate_health_passed','manual_hardware_required','generated_needs_manual_hardware','technical_blocker','health_only','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled']);
3
  const DEFAULT_TIMELINE=[{label:'Bootstrap',status:'pending'},{label:'Dependencies',status:'pending'},{label:'Auth',status:'pending'},{label:'Model analysis',status:'pending'},{label:'Workspace',status:'pending'},{label:'Node/npm',status:'pending'},{label:'Pi install',status:'pending'},{label:'Pi config',status:'pending'},{label:'Pi run',status:'pending'},{label:'Pi verification',status:'pending'},{label:'Metadata sanitize',status:'pending'},{label:'Requirements sanitize',status:'pending'},{label:'Hardware strategy',status:'pending'},{label:'Create with hardware',status:'pending'},{label:'Create Space',status:'pending'},{label:'Repair pass',status:'pending'},{label:'Upload files',status:'pending'},{label:'Space runtime',status:'pending'},{label:'Space logs',status:'pending'},{label:'API validation',status:'pending'},{label:'Live wait',status:'pending'},{label:'Generation smoke',status:'pending'},{label:'Inference gate',status:'pending'},{label:'Report',status:'pending'},{label:'Done',status:'pending'},{label:'Failure',status:'pending'}];
4
+
5
+ function bindOnce(el,eventName,key,handler,options){
6
+ if(!el)return;
7
+ const attr='bound'+String(key||eventName).replace(/[^a-zA-Z0-9]/g,'');
8
+ if(el.dataset[attr]==='true')return;
9
+ el.dataset[attr]='true';
10
+ el.addEventListener(eventName,handler,options);
11
+ }
12
+ function bindAllOnce(selector,eventName,key,handler,options){
13
+ document.querySelectorAll(selector).forEach((el,index)=>bindOnce(el,eventName,`${key}${index}`,handler,options));
14
+ }
15
+
16
+ function updateImplementationModeHelp(){
17
+ const value=formValue('implementationMode')||'full-inference-gated';
18
+ const copy=implementationModeCopy(value);
19
+ const el=$('implementationModeHelp');
20
+ if(!el)return;
21
+ el.innerHTML=`<strong>${escapeHtml(copy.title)}</strong><span>${escapeHtml(copy.description)}</span>`;
22
+ }
23
+
24
  function saveActiveRun(){try{localStorage.setItem('asf.activeRun',JSON.stringify({runId:state.runId,bucketName:state.bucketName,links:state.activeLinks||{}}))}catch(_){}}
25
  function clearSavedRun(){try{localStorage.removeItem('asf.activeRun')}catch(_){} state.spaceLinksUnlockedFor=null}
26
  function artifactUrl(runId,bucketSource){if(!runId||!bucketSource)return'';return `https://huggingface.co/buckets/${bucketSource}/tree/runs/${runId}`}
 
92
  }
93
  return uniqueStrings(names.map(normalizeApiName));
94
  }
95
+ function endpointSchemaEntriesFromApiSchema(schema={}){
96
+ const raw=schema.schema&&typeof schema.schema==='object'?schema.schema:schema;
97
+ const named=raw.named_endpoints||raw.endpoints||raw.dependencies||[];
98
+ if(Array.isArray(named))return named;
99
+ if(named&&typeof named==='object')return Object.entries(named).map(([api_name,value])=>({api_name,...(value&&typeof value==='object'?value:{})}));
100
+ return [];
101
+ }
102
+ function endpointNameFromEntry(entry={}){
103
+ return normalizeApiName(entry.api_name||entry.api_name_for_predict||entry.name||entry.endpoint||entry.path||entry.id||'');
104
+ }
105
+ function validationEndpointParameters(detail={},endpointName=''){
106
  const smoke=detail.generation_smoke||detail.summary?.generation_smoke||detail.state?.generation_smoke||{};
107
  const schema=detail.api_schema||detail.tests?.api_schema||{};
108
+ const selected=normalizeApiName(endpointName||smoke.api_name||schema.selected_api_name||formValue('validateApi')||'');
109
+ const entries=endpointSchemaEntriesFromApiSchema(schema);
110
+ const match=entries.find(entry=>endpointNameFromEntry(entry)===selected);
111
+ const possible=match?.parameters||match?.inputs||match?.input_components||match?.components||smoke.endpoint_parameters||schema.endpoint_parameters||schema.schema?.endpoint_parameters||[];
112
+ const params=Array.isArray(possible)?possible.map(item=>typeof item==='string'?item:(item?.parameter_name||item?.label||item?.name||item?.id||item?.component||'')):[];
113
+ return uniqueStrings(params);
114
+ }
115
+ function applyValidationEndpoint(endpointName,detail={}){
116
+ const name=normalizeApiName(endpointName);
117
+ setInputValue('validateApi',name);
118
+ const input=$('validateApi');
119
+ if(input){input.dispatchEvent(new Event('input',{bubbles:true}));input.focus({preventScroll:true})}
120
+ renderValidationEndpoints(detail||state.validationDetail||{});
121
+ showMessage(`Selected Gradio endpoint ${name}.`,'success');
122
+ }
123
+ function bindValidationEndpointPicker(root){
124
+ if(!root||root.dataset.boundEndpointPicker==='true')return;
125
+ root.dataset.boundEndpointPicker='true';
126
+ root.addEventListener('click',e=>{
127
+ const btn=e.target.closest('[data-endpoint-name]');
128
+ if(!btn)return;
129
+ e.preventDefault();
130
+ applyValidationEndpoint(btn.dataset.endpointName,state.validationDetail||{});
131
+ });
132
  }
133
  function renderValidationEndpoints(detail={}){
134
  const root=$('spaceTestEndpoints');
135
  if(!root)return;
136
+ bindValidationEndpointPicker(root);
137
  const names=validationEndpointNames(detail);
138
  const selected=normalizeApiName(formValue('validateApi')||detail.generation_smoke?.api_name||detail.api_schema?.selected_api_name||'');
139
+ const params=validationEndpointParameters(detail,selected);
140
  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}
141
+ 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)} as API name"><span>${escapeHtml(name)}</span><em>${name===selected?'selected':'use'}</em></button>`).join('');
142
+ const selectedLine=`<div class="endpoint-selected-row"><span>Selected endpoint</span><strong>${escapeHtml(selected)}</strong><button class="secondary small" type="button" id="useSelectedEndpoint" data-endpoint-name="${escapeHtml(selected)}">Use in form</button></div>`;
143
+ const paramItems=params.slice(0,12).map(param=>`<em title="${escapeHtml(param)}">${escapeHtml(param)}</em>`).join('');
144
+ const paramLine=params.length?`<div class="endpoint-params"><strong>Params</strong><span>${paramItems}${params.length>12?'<em>…</em>':''}</span></div>`:'<div class="endpoint-params muted"><strong>Params</strong><span>No parameters discovered for this endpoint yet.</span></div>';
145
+ root.innerHTML=`<div class="endpoint-chip-list">${chips}</div>${selectedLine}${paramLine}`;
146
  }
147
  function setGeneratedSpaceLinksEnabled(links={},enabled=false){
148
  const space=enabled?links.target_space_url:'';
 
154
  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}
155
  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)}
156
  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}
157
+ 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')}
158
+ 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')}}
159
  function setBucketFeedback(mode='checking',message='Checking private bucket access…'){
160
  const orb=$('bucketInlineOrb');
161
  if(orb)orb.className='bucket-status-orb '+mode;
162
  setText('bucketFeedbackText',message);
163
  }
164
+ 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()}
165
  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}
166
  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':''}`}
167
+
168
+ function normalizeModelInput(value){return String(value||'').trim().replace('https://huggingface.co/','').split('?')[0].split('#')[0].replace(/^\/+|\/+$/g,'')}
169
+ function modelScanMatchesCurrent(){return state.modelScan?.ok&&normalizeModelInput(state.modelScan.model_id)===normalizeModelInput(formValue('modelId'))}
170
+ 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'}
171
+ function renderModelPreScan(scan=null){
172
+ const card=$('modelPreScanCard'),badge=$('modelPreScanBadge'),signals=$('modelPreScanSignals'),ackWrap=$('modelRiskAckWrap');
173
+ if(!card)return;
174
+ const current=normalizeModelInput(formValue('modelId'));
175
+ const matches=scan&&scan.ok&&normalizeModelInput(scan.model_id)===current;
176
+ if(!scan||!matches){
177
+ card.className='model-prescan-card required';
178
+ setText('modelPreScanTitle','Pre-scan required');
179
+ 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.');
180
+ if(badge){badge.textContent='Required';badge.className='badge neutral'}
181
+ if(signals){signals.hidden=true;signals.innerHTML=''}
182
+ if(ackWrap)ackWrap.hidden=true;
183
+ return;
184
+ }
185
+ const verdict=String(scan.verdict||'caution').toLowerCase();
186
+ const cls=scanVerdictClass(verdict);
187
+ card.className='model-prescan-card '+cls;
188
+ setText('modelPreScanTitle',`${verdict.charAt(0).toUpperCase()+verdict.slice(1)} Β· score ${scan.score ?? 'β€”'}/100`);
189
+ setText('modelPreScanSummary',scan.summary||'Model metadata scan completed.');
190
+ if(badge){badge.textContent=verdict;badge.className='badge '+cls}
191
+ const good=(scan.good_signals||[]).slice(0,3);
192
+ const risk=(scan.risk_signals||[]).slice(0,4);
193
+ const recs=(scan.recommendations||[]).slice(0,2);
194
+ if(signals){
195
+ signals.hidden=false;
196
+ 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>`:''}`;
197
+ }
198
+ if(ackWrap)ackWrap.hidden=!(verdict==='risky');
199
+ }
200
+ function resetModelPreScan(){state.modelScan=null;const ack=$('modelRiskAck');if(ack)ack.checked=false;renderModelPreScan(null);setBuildLaunchEnabled()}
201
+ 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.')}
202
+ async function scanModel(){
203
+ const model=formValue('modelId');
204
+ if(!model){showMessage('Enter a model card URL or model ID before scanning.',true);return}
205
+ setButtonBusy('scanModel',true,'Scanning…');
206
+ const badge=$('modelPreScanBadge');if(badge){badge.textContent='Scanning';badge.className='badge running'}
207
+ setText('modelPreScanTitle','Scanning model metadata…');
208
+ setText('modelPreScanSummary','Reading the model card, Hub metadata and small config files. We do not download model weights.');
209
+ try{
210
+ const result=await apiPost('/api/models/pre-scan',{model_id:model});
211
+ state.modelScan=result;
212
+ const ack=$('modelRiskAck');if(ack)ack.checked=false;
213
+ renderModelPreScan(result);
214
+ setBuildLaunchEnabled();
215
+ showMessage(`Model pre-scan: ${result.verdict} (${result.score}/100)`,result.verdict==='safe'?'success':(result.verdict==='unsupported'||result.verdict==='risky'?true:'warning'));
216
+ }catch(e){state.modelScan=null;renderModelPreScan(null);showMessage(e.message,true)}
217
+ finally{setButtonBusy('scanModel',false)}
218
+ }
219
+
220
+ 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.';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''}
221
  function parseJsonField(id,fallback){const raw=formValue(id);if(!raw)return fallback;return JSON.parse(raw)}
222
  function prettyJson(value){return JSON.stringify(value,null,2)}
223
  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''}
 
314
  if(!value)return'/generate';
315
  return value.startsWith('/')?value:'/'+value;
316
  }
317
+ function firstPositiveNumber(...values){
318
+ for(const value of values.flat()){
319
+ const n=Number(value);
320
+ if(Number.isFinite(n)&&n>0)return n;
321
+ }
322
+ return null;
323
+ }
324
+ function formatLatencySeconds(value){
325
+ const n=Number(value);
326
+ if(!Number.isFinite(n)||n<=0)return'β€”';
327
+ if(n<10)return`${n.toFixed(2).replace(/\.00$/,'')}s`;
328
+ if(n<60)return`${n.toFixed(1).replace(/\.0$/,'')}s`;
329
+ return fmtSeconds(Math.round(n));
330
+ }
331
  function parsedJsonOrNull(value){
332
  if(value==null||value==='')return null;
333
  try{return typeof value==='string'?JSON.parse(value):value}catch(_){return null}
 
367
  const runId=data.run_id||summary.run_id||state.validationRunId||'β€”';
368
  setText('spaceTestOutput',runId);
369
  setText('lastPolled',formatTime(data.updated_at||summary.updated_at||data.state?.updated_at||data.created_at||summary.created_at||new Date().toISOString()));
370
+ const latency=firstPositiveNumber(smoke.latency_seconds,smoke.observed_latency_seconds,summary.latency_seconds,summary.observed_latency_seconds,data.latency_seconds,data.observed_latency_seconds);
371
+ setText('spaceTestLatency',latency?formatLatencySeconds(latency):'β€”');
372
  setText('spaceTestVerdict',badgeLabel(status));
373
  const deleteBtn=$('deleteValidationRun');
374
  if(deleteBtn){deleteBtn.disabled=!(runId&&runId!=='β€”');deleteBtn.onclick=()=>deleteRunById(runId,{kind:'validation',detail:data});}
 
467
  console.warn('report refresh failed',e);
468
  }
469
  }
470
+ 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 p=await apiGet(`/api/runs/${encodeURIComponent(state.runId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}${includeLogs?'&include_job_logs=1':''}`);state.activeRunProgress=p;state.pollErrors=0;if(p.bucket_source)state.bucketSource=p.bucket_source;renderProgress(p);renderEvents(p.events||[]);renderMetrics(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)}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;refreshRunReportPreview(state.runId)}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 readRunLatencyPayload(p={}){
471
+ const smoke=p.generation_smoke||p.tests?.generation_smoke||p.summary?.generation_smoke||p.state?.generation_smoke||{};
472
+ const gate=p.inference_gate||p.summary?.inference_gate||{};
473
+ const summary=p.summary||{};
474
+ const latency=firstPositiveNumber(smoke.latency_seconds,smoke.observed_latency_seconds,summary.latency_seconds,summary.observed_latency_seconds,p.latency_seconds,p.observed_latency_seconds);
475
+ const duration=firstPositiveNumber(smoke.recommended_zerogpu_duration_seconds,smoke.recommended_duration_seconds,summary.recommended_zerogpu_duration_seconds,summary.recommended_duration_seconds,gate.recommended_zerogpu_duration_seconds,gate.zero_gpu_duration_recommendation);
476
+ const source=smoke.recommendation_source||summary.recommendation_source||gate.recommendation_source||'';
477
+ return {smoke,gate,latency,duration,source};
478
+ }
479
+ function renderActiveLatency(p={}){
480
+ const card=$('activeLatencyCard');
481
+ if(!card)return;
482
+ const {latency,duration,source}=readRunLatencyPayload(p);
483
+ const hasLatency=Number.isFinite(latency)&&latency>0;
484
+ const hasDuration=Number.isFinite(duration)&&duration>0;
485
+ setText('activeLatencyValue',hasLatency?formatLatencySeconds(latency):'β€”');
486
+ setText('activeZeroGpuDuration',hasDuration?fmtSeconds(Math.round(duration)):'β€”');
487
+ setText('activeLatencySource',hasLatency?(source||'live smoke test'):'measured when smoke test passes');
488
+ const note=$('activeLatencyNote');
489
+ if(note){
490
+ note.textContent=hasDuration
491
+ ? 'This measured latency is normally used to tune the `@spaces.GPU(duration=...)` decorator for ZeroGPU when the generated Space uses ZeroGPU.'
492
+ : 'When available, the live smoke-test latency is used to recommend the `@spaces.GPU(duration=...)` value for ZeroGPU apps.';
493
+ }
494
+ card.classList.toggle('has-latency',hasLatency);
495
+ }
496
+ 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>`}
497
  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.'}}
498
  async function cancelActiveRun(){
499
  if(!state.runId){showMessage('Select a running run before cancelling.', 'warning');return}
 
513
  }
514
  async function resumeLatestRun(){setButtonBusy('resumeLatestRun',true,'Resuming…');try{await loadRuns(true);const data=await apiGet(`/api/runs/resumable?bucket_name=${encodeURIComponent(state.bucketName)}`);if(!data.run){showMessage('No latest run found in this bucket.','warning');return}state.runId=data.run.run_id;if(data.bucket_source)state.bucketSource=data.bucket_source;setText('detailRunId',state.runId);setBuildModeInspecting(state.runId);setQuickLinks(data.view?.links||{},data.view||{});if(data.view)renderRunViewModel(data.view,{run_id:state.runId,progress:data.view.header?.status==='succeeded'?100:0,events:data.view.activity||[]});startPolling(state.runId);showMessage(`Resumed ${state.runId}`,'success')}catch(e){showMessage(e.message,true)}finally{setButtonBusy('resumeLatestRun',false)}}
515
  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'})}
516
+ function bindNavigation(){
517
+ bindAllOnce('.nav-item','click','navItem',e=>switchView(e.currentTarget.dataset.view||'build'));
518
+ bindAllOnce('[data-view-target]','click','viewTarget',e=>switchView(e.currentTarget.dataset.viewTarget||'build'));
519
+ bindAllOnce('[data-center-tab]','click','centerTab',e=>switchCenterTab(e.currentTarget.dataset.centerTab||'active'));
520
+ }
521
+ function bindRunFilters(){
522
+ bindAllOnce('.filter[data-status]','click','runStatusFilter',e=>{
523
+ state.runStatusFilter=e.currentTarget.dataset.status||'all';
524
+ state.runPage=0;
525
+ renderRunsFromCache();
526
+ });
527
+ for(const id of ['runSearch','runSearchFull'])bindOnce($(id),'input',`runSearch${id}`,e=>{
528
+ state.runSearch=e.target.value||'';
529
+ state.runPage=0;
530
+ for(const other of ['runSearch','runSearchFull'])if($(other)&&$(other)!==e.target)$(other).value=state.runSearch;
531
+ clearTimeout(state.runSearchTimer);
532
+ state.runSearchTimer=setTimeout(renderRunsFromCache,80);
533
+ });
534
+ }
535
+ function bindActions(){
536
+ $('launchBuild')?.setAttribute('data-requires-bucket','true');
537
+ $('launchBuild')?.setAttribute('data-action-button','true');
538
+ $('launchValidate')?.setAttribute('data-action-button','true');
539
+ const actions={
540
+ launchBuild:launchBuild,
541
+ scanModel:scanModel,
542
+ launchValidate:launchValidate,
543
+ prepareNewBuild:prepareNewBuild,
544
+ manualGoValidate:prepareValidationFromActiveRun,
545
+ activePrefillSpaceTest:prepareValidationFromActiveRun,
546
+ checkBucket:()=>checkBucket(true),
547
+ createBucket:createBucket,
548
+ refreshRuns:()=>loadRuns(true),
549
+ refreshRunsHome:()=>loadRuns(true),
550
+ resumeLatestRun:resumeLatestRun,
551
+ resumeLatestRunFull:resumeLatestRun,
552
+ cancelRun:cancelActiveRun,
553
+ deleteActiveRun:()=>deleteRunById(state.runId,{kind:'active',detail:state.selectedRunDetail||state.activeRunProgress||{}}),
554
+ deleteValidationRun:()=>deleteRunById(state.validationRunId,{kind:'validation',detail:state.validationDetail||{}}),
555
+ };
556
+ Object.entries(actions).forEach(([id,handler])=>bindOnce($(id),'click',`action${id}`,handler));
557
+ bindOnce($('modelId'),'input','modelPreScanReset',resetModelPreScan);
558
+ bindOnce($('modelRiskAck'),'change','modelRiskAck',setBuildLaunchEnabled);
559
+ ['tryZeroGpu','preferredHardware','fallbackHardware','allowFallback'].forEach(id=>bindOnce($(id),'change',`hardware${id}`,()=>setText('hardwarePlan',hardwarePlanLabel())));
560
+ bindOnce($('implementationMode'),'change','implementationModeHelp',updateImplementationModeHelp);
561
+ }
562
+ bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());updateImplementationModeHelp();setBuildModeNew();renderTimeline(DEFAULT_TIMELINE);loadAppInfo();
563
 
564
  // Pass 47 intentionally removed automatic switchView('progress') after launch and run selection.
565
+ // Compatibility note for v108 regression: implementationMode')?.addEventListener('change',updateImplementationModeHelp is now bound through bindOnce().
web/static/components.js CHANGED
@@ -3,6 +3,17 @@ function formValue(id){return ($(id)?.value||'').trim()}
3
  function setText(id,value){const el=$(id);if(el)el.textContent=value??'β€”'}
4
  function setInputValue(id,value){const el=$(id);if(el)el.value=value||''}
5
  function escapeHtml(value){return String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}
 
 
 
 
 
 
 
 
 
 
 
6
  function fmtSeconds(s){if(s==null||s==='')return'β€”';s=Math.max(0,Number(s)||0);const h=Math.floor(s/3600),m=Math.floor((s%3600)/60),r=s%60;if(h)return`${h}h ${m}m`;return m?`${m}m ${r}s`:`${r}s`}
7
  function shortText(value,max=42){value=String(value||'β€”');return value.length>max?value.slice(0,max-1)+'…':value}
8
  function formatTime(ts){if(!ts)return'β€”';try{return new Date(ts).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'})}catch(_){return String(ts).slice(11,19)||'β€”'}}
 
3
  function setText(id,value){const el=$(id);if(el)el.textContent=value??'β€”'}
4
  function setInputValue(id,value){const el=$(id);if(el)el.value=value||''}
5
  function escapeHtml(value){return String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}
6
+
7
+ const IMPLEMENTATION_MODE_COPY={
8
+ 'full-inference-gated':{title:'Strict inference',short:'Strict',description:'Requires a real working inference endpoint. The run fails if the generated Space only boots without producing a valid output.'},
9
+ 'full-inference-attempt':{title:'Best effort inference',short:'Best effort',description:'Tries to build real inference, but allows partial prototypes or manual follow-up when the model or hardware needs extra work.'},
10
+ 'safe-scaffold':{title:'Demo scaffold only',short:'Demo only',description:'Only requires a bootable Gradio demo. This mode does not guarantee that model inference works.'}
11
+ };
12
+ function implementationModeCopy(value){return IMPLEMENTATION_MODE_COPY[String(value||'').trim()]||IMPLEMENTATION_MODE_COPY['full-inference-gated']}
13
+ function implementationModeLabel(value,{short=false}={}){const copy=implementationModeCopy(value);return short?(copy.short||copy.title):copy.title}
14
+ function implementationModeDescription(value){return implementationModeCopy(value).description}
15
+ function firstNonEmpty(...values){for(const value of values){if(value!==undefined&&value!==null&&String(value).trim())return String(value).trim()}return''}
16
+
17
  function fmtSeconds(s){if(s==null||s==='')return'β€”';s=Math.max(0,Number(s)||0);const h=Math.floor(s/3600),m=Math.floor((s%3600)/60),r=s%60;if(h)return`${h}h ${m}m`;return m?`${m}m ${r}s`:`${r}s`}
18
  function shortText(value,max=42){value=String(value||'β€”');return value.length>max?value.slice(0,max-1)+'…':value}
19
  function formatTime(ts){if(!ts)return'β€”';try{return new Date(ts).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'})}catch(_){return String(ts).slice(11,19)||'β€”'}}
web/static/progress.js CHANGED
@@ -443,13 +443,30 @@ function contextualEventLabel(item={},klass=''){
443
  if(klass.includes('recovered'))return`${raw||'failed'} Β· superseded`;
444
  return raw;
445
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  function renderActivityFeed(items){
447
  const root=$('activityFeed');
448
  if(!root)return;
449
  const list=mergeStableEvents(String(state.runId||''),items||[]);
450
- const visible=[...list].reverse();
451
  const signature=visible.map(item=>`${item.ts||''}:${item.step||''}:${item.status||''}:${item.message||''}`).join('|');
452
  if(root.dataset.signature===signature)return;
 
453
  root.dataset.signature=signature;
454
  root.classList.add('show-all');
455
  if(!list.length){
@@ -462,9 +479,11 @@ function renderActivityFeed(items){
462
  const label=contextualEventLabel(item,klass);
463
  const reason=eventReason(item);
464
  const reasonHtml=reason&&reason!==String(item.message||'').trim()?`<p class="event-reason">${escapeHtml(reason)}</p>`:'';
465
- return `<div class="activity-row ${klass}"><time>${escapeHtml(formatTime(item.ts))}</time><span class="activity-dot"></span><div><strong>${escapeHtml(item.message||'Event received')}</strong><p>${escapeHtml(item.step||'event')} Β· ${escapeHtml(label||'')}</p>${reasonHtml}</div><em>${escapeHtml(item.agent||'Factory Agent')}</em></div>`;
 
 
466
  }).join('');
467
- root.scrollTop=0;
468
  }
469
 
470
  function renderDiagnostics(d={},header={}){
 
443
  if(klass.includes('recovered'))return`${raw||'failed'} Β· superseded`;
444
  return raw;
445
  }
446
+ function eventIconFor(item={},klass=''){
447
+ const step=String(item.step||'').toLowerCase();
448
+ const status=String(item.status||'').toLowerCase();
449
+ if(klass.includes('error')||status.includes('fail')||status.includes('error'))return'⚠️';
450
+ if(klass.includes('hardware-fallback')||step.includes('hardware'))return'🧠';
451
+ if(step.includes('pi_')||step.includes('pi'))return'πŸ€–';
452
+ if(step.includes('trace'))return'🧾';
453
+ if(step.includes('space')||step.includes('upload'))return'πŸš€';
454
+ if(step.includes('api')||step.includes('smoke')||step.includes('inference'))return'⚑';
455
+ if(step.includes('report'))return'πŸ“„';
456
+ if(step.includes('auth'))return'πŸ”';
457
+ if(step.includes('model'))return'πŸ”Ž';
458
+ if(status.includes('success')||status==='done')return'βœ“';
459
+ return'β€’';
460
+ }
461
+ // v105 keeps v64 compact-list invariant: const visible=[...list].reverse()
462
  function renderActivityFeed(items){
463
  const root=$('activityFeed');
464
  if(!root)return;
465
  const list=mergeStableEvents(String(state.runId||''),items||[]);
466
+ const visible=[...list].slice(-30).reverse();
467
  const signature=visible.map(item=>`${item.ts||''}:${item.step||''}:${item.status||''}:${item.message||''}`).join('|');
468
  if(root.dataset.signature===signature)return;
469
+ const shouldKeepLatestVisible=root.scrollTop<24||!root.dataset.signature;
470
  root.dataset.signature=signature;
471
  root.classList.add('show-all');
472
  if(!list.length){
 
479
  const label=contextualEventLabel(item,klass);
480
  const reason=eventReason(item);
481
  const reasonHtml=reason&&reason!==String(item.message||'').trim()?`<p class="event-reason">${escapeHtml(reason)}</p>`:'';
482
+ const icon=eventIconFor(item,klass);
483
+ const step=String(item.step||'event').replace(/_/g,' ');
484
+ return `<div class="activity-row ${klass}"><time>${escapeHtml(formatTime(item.ts))}</time><span class="activity-icon" aria-hidden="true">${escapeHtml(icon)}</span><div class="activity-copy"><strong title="${escapeHtml(item.message||'Event received')}">${escapeHtml(item.message||'Event received')}</strong><p title="${escapeHtml(step+' Β· '+(label||''))}"><span>${escapeHtml(step)}</span><em>${escapeHtml(label||'')}</em></p>${reasonHtml}</div><em>${escapeHtml(item.agent||'Factory Agent')}</em></div>`;
485
  }).join('');
486
+ if(shouldKeepLatestVisible)root.scrollTop=0;
487
  }
488
 
489
  function renderDiagnostics(d={},header={}){
web/static/runs.js CHANGED
@@ -21,9 +21,29 @@ function sortRunsNewestFirst(runs=[]){
21
  function filteredRuns(){return (state.runsCache||[]).filter(r=>runMatchesStatus(r,state.runStatusFilter||'all')&&runMatchesQuery(r,state.runSearch||''))}
22
  function renderRunStats(){const counts={all:state.runsCache.length,running:0,full_inference_success:0,manual_hardware_required:0,failed:0,stale:0};for(const r of state.runsCache){if(runMatchesStatus(r,'running'))counts.running+=1;if(runMatchesStatus(r,'full_inference_success'))counts.full_inference_success+=1;if(runMatchesStatus(r,'manual_hardware_required'))counts.manual_hardware_required+=1;if(runMatchesStatus(r,'failed'))counts.failed+=1;if(runMatchesStatus(r,'stale'))counts.stale+=1}document.querySelectorAll('.filter[data-status]').forEach(btn=>{const status=btn.dataset.status||'all';const label={all:'All',running:'Running',full_inference_success:'Success',manual_hardware_required:'Manual',failed:'Failed',technical_blocker:'Blockers',stale:'Stale'}[status]||status;const count=counts[status];btn.innerHTML=typeof count==='number'?`${escapeHtml(label)} <span>${count}</span>`:escapeHtml(label);btn.classList.toggle('active',status===state.runStatusFilter)})}
23
  function runCardKind(run={}){return isValidationRunSummary(run)?'Space Test':'Build'}
24
- function runCardMeta(run={}){const items=[];if(run.model_id&&!isValidationRunSummary(run))items.push(['Model',shortText(run.model_id,42)]);if(run.target_space)items.push(['Space',shortText(run.target_space,42)]);if(run.selected_hardware&&!isValidationRunSummary(run))items.push(['HW',run.selected_hardware]);const latency=runStatLatency(run);if(latency&&isValidationRunSummary(run))items.push(['Latency',latency]);const t=runStatTime(run);if(t&&t!=='β€”')items.push(['Updated',t]);return items}
25
- function renderRunRows(runs,compact=false){return (runs||[]).map(r=>{const validation=isValidationRunSummary(r);const kind=runCardKind(r);const status=r.status||'unknown';const meta=runCardMeta(r).slice(0,compact?3:4).map(([k,v])=>`<span><b>${escapeHtml(k)}</b>${escapeHtml(v)}</span>`).join('');return `<div class="run-row run-card ${validation?'validation-run':''}" role="button" tabindex="0" data-run="${escapeHtml(r.run_id)}" title="${validation?'Validation run':'Build run'}"><div class="run-card-main"><span class="dot ${statusClass(status)}"></span><div class="run-card-copy"><div class="run-card-title"><strong class="run-id" title="${escapeHtml(r.run_id)}">${shortText(r.run_id,compact?28:34)}</strong><em class="kind-chip ${validation?'validation':'build'}">${escapeHtml(kind)}</em></div><div class="run-card-meta">${meta||'<span><b>Run</b>No metadata yet</span>'}</div></div></div><div class="run-card-side"><em class="pill ${statusClass(status)}">${escapeHtml(badgeLabel(status))}</em><button class="icon-btn danger" type="button" data-run-delete="${escapeHtml(r.run_id)}" title="Delete this run" aria-label="Delete run ${escapeHtml(r.run_id)}">Γ—</button></div></div>`}).join('')}
26
- function renderRunPagination(total){const root=$('runsPagination');if(!root)return;const perPage=state.runsPerPage||5;const pages=Math.max(1,Math.ceil(total/perPage));state.runPage=Math.min(Math.max(0,state.runPage||0),pages-1);if(total<=perPage){root.innerHTML='';return}root.innerHTML=`<button class="secondary small" data-page-action="prev" ${state.runPage<=0?'disabled':''}>β€Ή</button><span>Page ${state.runPage+1} / ${pages}</span><button class="secondary small" data-page-action="next" ${state.runPage>=pages-1?'disabled':''}>β€Ί</button>`;root.querySelectorAll('[data-page-action]').forEach(btn=>btn.addEventListener('click',()=>{state.runPage+=(btn.dataset.pageAction==='next'?1:-1);renderRunsFromCache()}))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  function isValidationRunSummary(run={}){return isValidationRunDetail({summary:run,state:run,kind:run.kind,run_type:run.run_type})}
29
  function runStatTime(run={}){const raw=run.updated_at||run.created_at||run.started_at||run.cancelled_at||'';return raw?formatTime(raw):'β€”'}
@@ -108,8 +128,25 @@ function renderRunAggregateStats(){
108
  <strong>${issueItems.length?issueItems.map(([label,count])=>`${count} ${label}`).join(' Β· '):'No aggregated issues in indexed runs'}</strong>
109
  </div>`;
110
  }
111
- function renderRunsFromCache(){state.runsCache=sortRunsNewestFirst(state.runsCache||[]);const runs=filteredRuns();const perPage=state.runsPerPage||5;state.runPage=Math.min(Math.max(0,state.runPage||0),Math.max(0,Math.ceil(runs.length/perPage)-1));const visible=runs.slice(state.runPage*perPage,state.runPage*perPage+perPage);for(const [id,compact] of [['runsTableHome',true],['runsTable',false]]){const root=$(id);if(root)root.innerHTML=renderRunRows(visible,compact)||'<div class="empty">No runs match this filter.</div>'}renderRunStats();renderRunPagination(runs.length);renderRunAggregateStats();document.querySelectorAll('.run-row[data-run]').forEach(row=>{row.addEventListener('click',e=>{if(e.target.closest('[data-run-delete]'))return;selectRun(row.dataset.run)});row.addEventListener('keydown',e=>{if((e.key==='Enter'||e.key===' ')&&!e.target.closest('[data-run-delete]')){e.preventDefault();selectRun(row.dataset.run)}})});document.querySelectorAll('[data-run-delete]').forEach(btn=>btn.addEventListener('click',e=>{e.preventDefault();e.stopPropagation();const runId=btn.dataset.runDelete;const cached=cachedRunById(runId)||{};deleteRunById(runId,{kind:isValidationRunSummary(cached)?'validation':'active',detail:optimisticDetailFromSummary(cached),buttonId:null})}));const selectedId=state.centerTab==='validate'&&state.validationRunId?state.validationRunId:state.runId;if(selectedId)document.querySelectorAll('.run-row').forEach(row=>row.classList.toggle('selected',row.dataset.run===selectedId))}
112
- async function loadRuns(forceNetwork=false){const stale=Date.now()-(state.runsLastLoaded||0)>45000;const shouldFetch=forceNetwork||!state.runsCache.length||stale;try{if(shouldFetch){document.querySelectorAll('.runs-panel,.full-runs-panel').forEach(x=>x.classList.add('is-refreshing'));const data=await apiGet(`/api/runs?limit=100&bucket_name=${encodeURIComponent(state.bucketName)}`);state.runsCache=sortRunsNewestFirst(data.runs||[]);state.runsLastLoaded=Date.now();if(data.bucket_source)state.bucketSource=data.bucket_source}renderRunsFromCache()}catch(e){for(const id of ['runsTable','runsTableHome']){const root=$(id);if(root)root.textContent='Could not load runs: '+e.message}if(forceNetwork)console.warn(e)}finally{document.querySelectorAll('.runs-panel,.full-runs-panel').forEach(x=>x.classList.remove('is-refreshing'))}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  function cachedRunById(runId){
114
  return (state.runsCache||[]).find(r=>r.run_id===runId)||null;
115
  }
@@ -300,6 +337,10 @@ function renderRunDetail(detail){
300
  setText('detailJobStatus',summary.status||detail.status||'β€”');
301
  setText('detailGateStatus',(detail.inference_gate||{}).status||summary.status||'β€”');
302
  setText('detailSpace',summary.target_space||detail.state?.target_space||'β€”');
 
 
 
 
303
  setText('detailHardware',summary.selected_hardware||summary.hardware||detail.state?.selected_hardware||'β€”');
304
  setText('detailTraces',Array.isArray(detail.events)?String(detail.events.length)+' events':summary.traces_count||'β€”');
305
  const validation=$('selectedValidation');
 
21
  function filteredRuns(){return (state.runsCache||[]).filter(r=>runMatchesStatus(r,state.runStatusFilter||'all')&&runMatchesQuery(r,state.runSearch||''))}
22
  function renderRunStats(){const counts={all:state.runsCache.length,running:0,full_inference_success:0,manual_hardware_required:0,failed:0,stale:0};for(const r of state.runsCache){if(runMatchesStatus(r,'running'))counts.running+=1;if(runMatchesStatus(r,'full_inference_success'))counts.full_inference_success+=1;if(runMatchesStatus(r,'manual_hardware_required'))counts.manual_hardware_required+=1;if(runMatchesStatus(r,'failed'))counts.failed+=1;if(runMatchesStatus(r,'stale'))counts.stale+=1}document.querySelectorAll('.filter[data-status]').forEach(btn=>{const status=btn.dataset.status||'all';const label={all:'All',running:'Running',full_inference_success:'Success',manual_hardware_required:'Manual',failed:'Failed',technical_blocker:'Blockers',stale:'Stale'}[status]||status;const count=counts[status];btn.innerHTML=typeof count==='number'?`${escapeHtml(label)} <span>${count}</span>`:escapeHtml(label);btn.classList.toggle('active',status===state.runStatusFilter)})}
23
  function runCardKind(run={}){return isValidationRunSummary(run)?'Space Test':'Build'}
24
+ function runImplementationMode(run={},detail={}){return firstNonEmpty(run.implementation_mode,run.summary?.implementation_mode,run.state?.implementation_mode,run.launch?.implementation_mode,detail.summary?.implementation_mode,detail.state?.implementation_mode,detail.launch?.implementation_mode)}
25
+ function runCardMeta(run={}){const items=[];const validation=isValidationRunSummary(run);if(run.model_id&&!validation)items.push(['Model',shortText(run.model_id,42)]);if(!validation){const mode=runImplementationMode(run);if(mode)items.push(['Goal',implementationModeLabel(mode,{short:true})])}if(run.target_space)items.push(['Space',shortText(run.target_space,42)]);if(run.selected_hardware&&!validation)items.push(['HW',run.selected_hardware]);const latency=runStatLatency(run);if(latency&&validation)items.push(['Latency',latency]);const t=runStatTime(run);if(t&&t!=='β€”')items.push(['Updated',t]);return items}
26
+ function renderRunRows(runs,compact=false){return (runs||[]).map(r=>{const validation=isValidationRunSummary(r);const kind=runCardKind(r);const status=r.status||'unknown';const meta=runCardMeta(r).slice(0,compact?3:4).map(([k,v])=>`<span><b>${escapeHtml(k)}</b><em title="${escapeHtml(v)}">${escapeHtml(v)}</em></span>`).join('');return `<div class="run-row run-card ${validation?'validation-run':''}" role="button" tabindex="0" data-run="${escapeHtml(r.run_id)}" title="${validation?'Validation run':'Build run'}"><div class="run-card-main"><span class="dot ${statusClass(status)}"></span><div class="run-card-copy"><div class="run-card-title"><strong class="run-id" title="${escapeHtml(r.run_id)}">${shortText(r.run_id,compact?28:34)}</strong><em class="kind-chip ${validation?'validation':'build'}">${escapeHtml(kind)}</em></div><div class="run-card-meta">${meta||'<span><b>Run</b><em>No metadata yet</em></span>'}</div></div></div><div class="run-card-side"><em class="pill ${statusClass(status)}" title="${escapeHtml(badgeLabel(status))}">${escapeHtml(badgeLabel(status))}</em><button class="icon-btn danger" type="button" data-run-delete="${escapeHtml(r.run_id)}" title="Delete this run" aria-label="Delete run ${escapeHtml(r.run_id)}">Γ—</button></div></div>`}).join('')}
27
+ function bindRunPagination(root){
28
+ if(!root||root.dataset.boundPagination==='true')return;
29
+ root.dataset.boundPagination='true';
30
+ root.addEventListener('click',e=>{
31
+ const btn=e.target.closest('[data-page-action]');
32
+ if(!btn||btn.disabled)return;
33
+ state.runPage+=(btn.dataset.pageAction==='next'?1:-1);
34
+ renderRunsFromCache();
35
+ });
36
+ }
37
+ function renderRunPagination(total){
38
+ const root=$('runsPagination');
39
+ if(!root)return;
40
+ bindRunPagination(root);
41
+ const perPage=state.runsPerPage||5;
42
+ const pages=Math.max(1,Math.ceil(total/perPage));
43
+ state.runPage=Math.min(Math.max(0,state.runPage||0),pages-1);
44
+ if(total<=perPage){root.innerHTML='';return}
45
+ root.innerHTML=`<button class="secondary small" data-page-action="prev" ${state.runPage<=0?'disabled':''}>β€Ή</button><span>Page ${state.runPage+1} / ${pages}</span><button class="secondary small" data-page-action="next" ${state.runPage>=pages-1?'disabled':''}>β€Ί</button>`;
46
+ }
47
 
48
  function isValidationRunSummary(run={}){return isValidationRunDetail({summary:run,state:run,kind:run.kind,run_type:run.run_type})}
49
  function runStatTime(run={}){const raw=run.updated_at||run.created_at||run.started_at||run.cancelled_at||'';return raw?formatTime(raw):'β€”'}
 
128
  <strong>${issueItems.length?issueItems.map(([label,count])=>`${count} ${label}`).join(' Β· '):'No aggregated issues in indexed runs'}</strong>
129
  </div>`;
130
  }
131
+ // v105 delegated row binding preserves v98 invariant: renderRunAggregateStats();document.querySelectorAll('.run-row[data-run]')
132
+ function bindRunRowEvents(root){
133
+ if(!root||root.dataset.boundRuns==='true')return;
134
+ root.dataset.boundRuns='true';
135
+ root.addEventListener('click',e=>{
136
+ const deleteBtn=e.target.closest('[data-run-delete]');
137
+ if(deleteBtn){e.preventDefault();e.stopPropagation();const runId=deleteBtn.dataset.runDelete;const cached=cachedRunById(runId)||{};deleteRunById(runId,{kind:isValidationRunSummary(cached)?'validation':'active',detail:optimisticDetailFromSummary(cached),buttonId:null});return}
138
+ const row=e.target.closest('.run-row[data-run]');
139
+ if(row)selectRun(row.dataset.run);
140
+ });
141
+ root.addEventListener('keydown',e=>{
142
+ if(e.key!=='Enter'&&e.key!==' ')return;
143
+ const row=e.target.closest('.run-row[data-run]');
144
+ if(!row||e.target.closest('[data-run-delete]'))return;
145
+ e.preventDefault();selectRun(row.dataset.run);
146
+ });
147
+ }
148
+ function renderRunsFromCache(){state.runsCache=sortRunsNewestFirst(state.runsCache||[]);const runs=filteredRuns();const perPage=state.runsPerPage||5;state.runPage=Math.min(Math.max(0,state.runPage||0),Math.max(0,Math.ceil(runs.length/perPage)-1));const visible=runs.slice(state.runPage*perPage,state.runPage*perPage+perPage);const selectedId=state.centerTab==='validate'&&state.validationRunId?state.validationRunId:state.runId;const signature=JSON.stringify({ids:visible.map(r=>[r.run_id,r.status,r.updated_at,r.target_space,r.model_id,r.latency_seconds,r.observed_latency_seconds]),filter:state.runStatusFilter,search:state.runSearch,page:state.runPage,selected:selectedId});for(const [id,compact] of [['runsTableHome',true],['runsTable',false]]){const root=$(id);if(!root)continue;bindRunRowEvents(root);if(root.dataset.signature!==signature){root.innerHTML=renderRunRows(visible,compact)||'<div class="empty">No runs match this filter.</div>';root.dataset.signature=signature}}renderRunStats();renderRunPagination(runs.length);renderRunAggregateStats();document.querySelectorAll('.run-row').forEach(row=>row.classList.toggle('selected',Boolean(selectedId)&&row.dataset.run===selectedId))}
149
+ async function loadRuns(forceNetwork=false){const stale=Date.now()-(state.runsLastLoaded||0)>45000;const shouldFetch=forceNetwork||!state.runsCache.length||stale;if(!shouldFetch){renderRunsFromCache();return state.runsCache||[]}if(state.runsLoadPromise&&!forceNetwork)return state.runsLoadPromise;document.querySelectorAll('.runs-panel,.full-runs-panel').forEach(x=>x.classList.add('is-refreshing'));state.runsLoadPromise=apiGet(`/api/runs?limit=100&bucket_name=${encodeURIComponent(state.bucketName)}`).then(data=>{state.runsCache=sortRunsNewestFirst(data.runs||[]);state.runsLastLoaded=Date.now();if(data.bucket_source)state.bucketSource=data.bucket_source;renderRunsFromCache();return state.runsCache}).catch(e=>{for(const id of ['runsTable','runsTableHome']){const root=$(id);if(root)root.textContent='Could not load runs: '+e.message}if(forceNetwork)console.warn(e);return state.runsCache||[]}).finally(()=>{state.runsLoadPromise=null;document.querySelectorAll('.runs-panel,.full-runs-panel').forEach(x=>x.classList.remove('is-refreshing'))});return state.runsLoadPromise}
150
  function cachedRunById(runId){
151
  return (state.runsCache||[]).find(r=>r.run_id===runId)||null;
152
  }
 
337
  setText('detailJobStatus',summary.status||detail.status||'β€”');
338
  setText('detailGateStatus',(detail.inference_gate||{}).status||summary.status||'β€”');
339
  setText('detailSpace',summary.target_space||detail.state?.target_space||'β€”');
340
+ const mode=runImplementationMode(summary,detail);
341
+ setText('detailImplementationMode',mode?implementationModeLabel(mode):'β€”');
342
+ const modeEl=$('detailImplementationMode');
343
+ if(modeEl&&mode){modeEl.title=implementationModeDescription(mode)}
344
  setText('detailHardware',summary.selected_hardware||summary.hardware||detail.state?.selected_hardware||'β€”');
345
  setText('detailTraces',Array.isArray(detail.events)?String(detail.events.length)+' events':summary.traces_count||'β€”');
346
  const validation=$('selectedValidation');