fffiloni commited on
Commit
f4a7aae
Β·
verified Β·
1 Parent(s): f06446e

Upload 7 files

Browse files
web/static/app.css CHANGED
@@ -133,10 +133,6 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
133
  .activity-row strong{font-size:12px}
134
  .activity-row p{margin:2px 0 0;color:var(--muted);font-size:11px}
135
  .activity-row em{font-style:normal;font-size:10px;border-radius:999px;padding:3px 7px;background:#f2f4f7;color:#344054}
136
- .report-home p{margin:0;color:#344054;font-size:12px;line-height:1.45}
137
- .report-home strong{display:none}
138
- .report-link{margin-top:8px}
139
-
140
  .search-wrap{position:relative}
141
  .search-wrap input{padding-right:34px}
142
  .search-wrap span{position:absolute;right:12px;top:50%;transform:translateY(-50%);color:var(--muted)}
@@ -487,12 +483,10 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
487
  .activity-row.error strong{color:#be123c}
488
 
489
  /* v78: report preview is useful by itself */
490
- .report-home{
491
- max-height:260px;
492
- overflow:auto;
493
- white-space:pre-wrap;
494
- }
495
- .report-home p{
496
- white-space:pre-wrap;
497
- }
498
- .report-link{display:none!important}
 
133
  .activity-row strong{font-size:12px}
134
  .activity-row p{margin:2px 0 0;color:var(--muted);font-size:11px}
135
  .activity-row em{font-style:normal;font-size:10px;border-radius:999px;padding:3px 7px;background:#f2f4f7;color:#344054}
 
 
 
 
136
  .search-wrap{position:relative}
137
  .search-wrap input{padding-right:34px}
138
  .search-wrap span{position:absolute;right:12px;top:50%;transform:translateY(-50%);color:var(--muted)}
 
483
  .activity-row.error strong{color:#be123c}
484
 
485
  /* v78: report preview is useful by itself */
486
+
487
+ /* v80: timeline failed-history / recovered feedback */
488
+ .step.status-warn{--ring-color:var(--amber)}
489
+ .step.status-warn:before{background:var(--amber)}
490
+ .step.status-warn .step-dot{--ring-color:var(--amber);color:#92400e}
491
+ .step.status-warn small{color:#92400e}
492
+ .top-actions .bucket-pill{display:none!important}
 
 
web/static/app.js CHANGED
@@ -27,7 +27,7 @@ function setOperation(message,type='info',opts={}){const el=$('operationStatus')
27
  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}
28
  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')}
29
  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')}}
30
- function setBucketReady(ok,message){state.bucketReady=Boolean(ok);const btn=$('launchBuild');if(btn)btn.disabled=state.busy||!state.bucketReady;setText('buildGateText',state.bucketReady?'Bucket ready. You can launch builds.':(message||'Create your private run bucket before launching builds.'));setText('bucketStatusText',state.bucketReady?`Bucket: ${state.bucketSource||'connected'}`:'Bucket not ready');setText('bucketInlineStatus',state.bucketReady?'Bucket ready':'Not ready');for(const id of ['bucketStatusDot','bucketInlineDot','sidebarBucketDot']){const dot=$(id);if(dot)dot.className='status-dot '+(state.bucketReady?'green':'amber-dot')}const inline=$('bucketInlineStatus');if(inline)inline.className='badge '+(state.bucketReady?'success':'warn');document.querySelectorAll('[data-requires-bucket="true"]').forEach(btn=>btn.disabled=state.busy||!state.bucketReady)}
31
  function effectivePreferredHardware(){const preferred=formValue('preferredHardware')||'zero-a10g';const fallback=formValue('fallbackHardware')||'a10g-large';const tryZero=$('tryZeroGpu')?.checked??true;if(tryZero)return preferred;if(preferred==='zero-a10g'||preferred.startsWith('zero'))return fallback;return preferred}
32
  function hardwarePlanLabel(){const tryZero=$('tryZeroGpu')?.checked??true;const preferred=effectivePreferredHardware();const fallback=formValue('fallbackHardware')||'a10g-large';const allow=$('allowFallback')?.checked??true;if(tryZero)return `ZeroGPU β†’ ${allow?fallback:'no fixed fallback'}`;return `${preferred}${allow&&fallback!==preferred?' β†’ '+fallback+' fallback':''}`}
33
  function validateBuildForm(){if(!state.bucketReady)return'Create or check your private run bucket before launching a build.';if(!formValue('modelId'))return'Enter a model card URL or model ID.';if(!formValue('targetSpace'))return'Enter a fresh target Space name.';if(!formValue('piModel'))return'Enter the Pi model to use.';return''}
@@ -48,7 +48,6 @@ function resetRunDetails(){
48
  const status=$('progressStatus');if(status)status.className='badge neutral';
49
  const fill=$('progressFill');if(fill){fill.style.width='0%';fill.classList.remove('is-running','progress-bump')}
50
  setQuickLinks({});
51
- const report=$('homeReportPreview');if(report)report.innerHTML='<strong>No run selected</strong><p>Select a run to preview its report and actions.</p>';
52
  const full=$('reportPreview');if(full)full.textContent='No report selected.';
53
  const manual=$('manualActionPanel');if(manual)manual.hidden=true;
54
 
@@ -81,14 +80,6 @@ async function refreshRunReportPreview(runId){
81
  const reportText=(detail.report||'No report available.').slice(0,8000);
82
  const report=$('reportPreview');
83
  if(report)report.textContent=reportText;
84
- const homeReport=$('homeReportPreview');
85
- if(homeReport){
86
- const summary=detail.summary||{};
87
- const view=detail.view||{};
88
- const header=view.header||{};
89
- const firstLine=reportText.split('\n').find(Boolean)||'No report available yet.';
90
- homeReport.innerHTML=`<strong>${escapeHtml(header.status_label||summary.status||'Run finished')}</strong><p>${escapeHtml(shortText(reportText,1200))}</p>`;
91
- }
92
  state.selectedRunDetail=detail;
93
  if(typeof renderSelectedLinks==='function')renderSelectedLinks({...detail.summary,bucket_source:detail.bucket_source},detail);
94
  if(typeof renderBlockers==='function')renderBlockers(detail);
@@ -119,7 +110,7 @@ async function resumeLatestRun(){setButtonBusy('resumeLatestRun',true,'Resuming
119
  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);const targetMap={build:'newBuildCard',progress:'runsExplorerPanel',validate:'spaceTestSection'};const target=$(targetMap[view]||'');if(target)target.scrollIntoView({behavior:'smooth',block:'start'})}
120
  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')))}
121
  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)})}
122
- 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);$('selectedValidateBtn')?.addEventListener('click',prepareValidationFromActiveRun);$('checkBucket')?.addEventListener('click',()=>checkBucket(true));$('bucketStatusPill')?.addEventListener('click',()=>checkBucket(true));$('createBucket')?.addEventListener('click',createBucket);$('refreshRuns')?.addEventListener('click',()=>loadRuns(true));$('refreshRunsHome')?.addEventListener('click',()=>loadRuns(true));$('resumeLatestRun')?.addEventListener('click',resumeLatestRun);$('cancelRun')?.addEventListener('click',cancelActiveRun);$('resumeLatestRunFull')?.addEventListener('click',resumeLatestRun);['tryZeroGpu','preferredHardware','fallbackHardware','allowFallback'].forEach(id=>$(id)?.addEventListener('change',()=>setText('hardwarePlan',hardwarePlanLabel())))}
123
  bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());setBuildModeNew();renderTimeline(DEFAULT_TIMELINE);loadAppInfo();
124
 
125
  // Pass 47 intentionally removed automatic switchView('progress') after launch and run selection.
 
27
  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}
28
  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')}
29
  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')}}
30
+ function setBucketReady(ok,message){state.bucketReady=Boolean(ok);const btn=$('launchBuild');if(btn)btn.disabled=state.busy||!state.bucketReady;setText('buildGateText',state.bucketReady?'Bucket ready. You can launch builds.':(message||'Create your private run bucket before launching builds.'));setText('bucketInlineStatus',state.bucketReady?'Bucket ready':'Not ready');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)}
31
  function effectivePreferredHardware(){const preferred=formValue('preferredHardware')||'zero-a10g';const fallback=formValue('fallbackHardware')||'a10g-large';const tryZero=$('tryZeroGpu')?.checked??true;if(tryZero)return preferred;if(preferred==='zero-a10g'||preferred.startsWith('zero'))return fallback;return preferred}
32
  function hardwarePlanLabel(){const tryZero=$('tryZeroGpu')?.checked??true;const preferred=effectivePreferredHardware();const fallback=formValue('fallbackHardware')||'a10g-large';const allow=$('allowFallback')?.checked??true;if(tryZero)return `ZeroGPU β†’ ${allow?fallback:'no fixed fallback'}`;return `${preferred}${allow&&fallback!==preferred?' β†’ '+fallback+' fallback':''}`}
33
  function validateBuildForm(){if(!state.bucketReady)return'Create or check your private run bucket before launching a build.';if(!formValue('modelId'))return'Enter a model card URL or model ID.';if(!formValue('targetSpace'))return'Enter a fresh target Space name.';if(!formValue('piModel'))return'Enter the Pi model to use.';return''}
 
48
  const status=$('progressStatus');if(status)status.className='badge neutral';
49
  const fill=$('progressFill');if(fill){fill.style.width='0%';fill.classList.remove('is-running','progress-bump')}
50
  setQuickLinks({});
 
51
  const full=$('reportPreview');if(full)full.textContent='No report selected.';
52
  const manual=$('manualActionPanel');if(manual)manual.hidden=true;
53
 
 
80
  const reportText=(detail.report||'No report available.').slice(0,8000);
81
  const report=$('reportPreview');
82
  if(report)report.textContent=reportText;
 
 
 
 
 
 
 
 
83
  state.selectedRunDetail=detail;
84
  if(typeof renderSelectedLinks==='function')renderSelectedLinks({...detail.summary,bucket_source:detail.bucket_source},detail);
85
  if(typeof renderBlockers==='function')renderBlockers(detail);
 
110
  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);const targetMap={build:'newBuildCard',progress:'runsExplorerPanel',validate:'spaceTestSection'};const target=$(targetMap[view]||'');if(target)target.scrollIntoView({behavior:'smooth',block:'start'})}
111
  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')))}
112
  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)})}
113
+ 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);$('selectedValidateBtn')?.addEventListener('click',prepareValidationFromActiveRun);$('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())))}
114
  bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());setBuildModeNew();renderTimeline(DEFAULT_TIMELINE);loadAppInfo();
115
 
116
  // Pass 47 intentionally removed automatic switchView('progress') after launch and run selection.
web/static/index.html ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Agentic Space Factory</title>
7
+ <link rel="stylesheet" href="/custom-static/app.css" />
8
+ </head>
9
+ <body>
10
+ <div class="app-shell onepage-shell">
11
+ <header class="topbar onepage-topbar">
12
+ <div class="brand">
13
+ <div class="brand-mark">πŸš€</div>
14
+ <div>
15
+ <h1>Agentic Space Factory</h1>
16
+ <p>Build, inspect, and validate private Hugging Face Spaces.</p>
17
+ </div>
18
+ </div>
19
+ <div class="top-actions">
20
+ <a class="user-chip" id="userChip" href="/oauth/huggingface/login">Sign in</a>
21
+ <button class="primary small" id="resumeLatestRun">Resume latest run</button>
22
+ <a class="logout-link" id="logoutLink" href="/logout" hidden>Logout</a>
23
+ </div>
24
+ </header>
25
+
26
+ <div class="toast-layer" id="toastLayer" aria-live="polite"><section class="operation-status floating-toast" id="operationStatus" hidden></section></div>
27
+
28
+ <main class="onepage-main">
29
+ <section class="auth-panel" id="authPanel" hidden></section>
30
+
31
+ <section class="workspace-grid">
32
+ <div class="left-stack">
33
+ <section class="panel bucket-setup-panel" id="bucketSetupPanel">
34
+ <div class="section-head compact">
35
+ <div><span class="panel-icon">β—ˆ</span><h2>Run storage</h2></div>
36
+ <span class="badge" id="bucketInlineStatus">Checking</span>
37
+ </div>
38
+ <p class="muted">Runs, reports, generated files, traces, and validation outputs are written to your private bucket.</p>
39
+ <div class="bucket-source-line"><span class="status-dot" id="bucketInlineDot"></span><strong id="bucketInlineLabel">loading…</strong></div>
40
+ <div class="inline-actions bucket-actions">
41
+ <button class="secondary small" id="checkBucket">Check bucket</button>
42
+ <button class="secondary small" id="createBucket">Create private bucket</button>
43
+ </div>
44
+ </section>
45
+
46
+ <section class="panel build-panel" id="newBuildCard">
47
+ <div class="section-head compact">
48
+ <div>
49
+ <span class="panel-icon">⌘</span>
50
+ <h2>New Build</h2>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="field-stack">
55
+ <label>Model card URL or model ID<input id="modelId" value="meta-llama/Meta-Llama-3.1-8B-Instruct" /></label>
56
+ <label>Target Space name<input id="targetSpace" placeholder="my-llama-31-8b-demo" /></label>
57
+ <div class="form-grid two-cols">
58
+ <label>Pi model<select id="piModel"><option value="Qwen/Qwen3-Coder-Next" selected>Qwen/Qwen3-Coder-Next</option></select></label>
59
+ <label>Implementation mode<select id="implementationMode">
60
+ <option value="full-inference-gated" selected>Strict real inference β€” validated Gradio app</option>
61
+ <option value="full-inference-attempt">Best-effort inference β€” permissive prototype</option>
62
+ <option value="safe-scaffold">Safe scaffold only β€” boots without inference guarantee</option>
63
+ </select></label>
64
+ </div>
65
+ <p class="muted cost-note">Pi uses Hugging Face Inference Providers for Qwen. Usage may consume paid inference credits.</p>
66
+ <div class="form-grid two-cols">
67
+ <label>Preferred GPU<select id="preferredHardware"><option value="zero-a10g" selected>ZeroGPU</option></select></label>
68
+ <label>Fallback GPU<select id="fallbackHardware"><option value="a10g-large" selected>A10G Large</option><option value="l40sx1">L40S x1</option><option value="cpu-basic">CPU Basic</option><option value="a100-large">A100 Large</option><option value="h200">H200</option></select></label>
69
+ </div>
70
+ <div class="toggle-list compact-toggles">
71
+ <label><span>Try ZeroGPU first</span><input id="tryZeroGpu" type="checkbox" checked /></label>
72
+ <label><span>Allow fixed GPU fallback</span><input id="allowFallback" type="checkbox" checked /></label>
73
+ </div>
74
+ <label>Expected output type<select id="buildExpectedOutput"><option value="text">Text</option><option value="image">Image</option><option value="video">Video</option><option value="audio">Audio</option><option value="any">Any</option></select></label>
75
+ </div>
76
+
77
+ <button class="primary full" id="launchBuild" disabled>Build private Space</button>
78
+ <p class="muted build-gate" id="buildGateText">Checking your run bucket before enabling builds…</p>
79
+ </div>
80
+
81
+ <section class="panel active-run-panel" id="activeRunPanel">
82
+ <div class="section-head compact">
83
+ <div><span class="panel-icon">β–·</span><h2>Active Run</h2></div>
84
+ <span class="badge neutral" id="progressStatus">Idle</span>
85
+ </div>
86
+
87
+ <div class="summary-meta active-meta">
88
+ <div><span>Run ID</span><strong id="detailRunId">β€”</strong></div>
89
+ <div><span>Target Space</span><strong id="detailSpace">β€”</strong></div>
90
+ <div><span>Model</span><strong id="selectedModelId">β€”</strong></div>
91
+ <div><span>Current step</span><strong id="currentStep">β€”</strong></div>
92
+ <div><span>Elapsed time</span><strong id="elapsed">β€”</strong></div>
93
+ </div>
94
+
95
+ <div class="active-run-actions active-run-cancel-only">
96
+ <button class="secondary danger" id="cancelRun" disabled>Cancel Job</button>
97
+ </div>
98
+
99
+ <div class="progress-caption"><span>Overall progress</span><strong id="progressPercent">0%</strong></div>
100
+ <div class="progress-bar"><div id="progressFill" style="width:0%"></div></div>
101
+ <div class="timeline" id="timeline"></div>
102
+
103
+ <div class="manual-action" id="manualActionPanel" hidden>
104
+ <div>
105
+ <strong id="manualActionTitle">Manual hardware required</strong>
106
+ <p id="manualActionText">Open Space settings, choose the recommended GPU, then run Space Test.</p>
107
+ </div>
108
+ <div class="inline-actions">
109
+ <a class="link-btn" id="manualOpenSettings" target="_blank">Open Space settings β†—</a>
110
+ <button class="secondary small" id="manualGoValidate">Prefill Space Test</button>
111
+ </div>
112
+ </div>
113
+
114
+ <section class="mini-panel latest-events-card">
115
+ <div class="card-title-row">
116
+ <h4>Latest events</h4>
117
+ </div>
118
+ <div id="activityFeed" class="activity-feed"><div class="empty">No activity yet.</div></div>
119
+ </section>
120
+ </section>
121
+
122
+ <section class="panel runs-explorer-panel" id="runsExplorerPanel">
123
+ <div class="section-head compact">
124
+ <div><span class="panel-icon">☰</span><h2>Runs Explorer</h2></div>
125
+ </div>
126
+
127
+ <div class="search-wrap"><input id="runSearchFull" placeholder="Search runs by ID, model, or space…" /><span>βŒ•</span></div>
128
+ <div class="run-filters" id="runFilters">
129
+ <button class="filter active" data-status="all">All</button>
130
+ <button class="filter" data-status="running">Running</button>
131
+ <button class="filter" data-status="full_inference_success">Success</button>
132
+ <button class="filter" data-status="manual_hardware_required">Manual</button>
133
+ <button class="filter" data-status="failed">Failed</button>
134
+ <button class="filter" data-status="stale">Stale</button>
135
+ </div>
136
+ <div id="runsTable" class="runs-table">Loading runs…</div>
137
+ <div id="runsPagination" class="runs-pagination"></div>
138
+
139
+ <section class="selected-run-card">
140
+ <div class="section-head compact">
141
+ <div><span class="panel-icon">β–£</span><h3>Selected run</h3></div>
142
+ </div>
143
+ <div class="detail-grid selected-grid">
144
+ <div><span>Run ID</span><strong id="selectedRunId">β€”</strong></div>
145
+ <div><span>Status</span><strong id="runDetailStatus">β€”</strong></div>
146
+ <div><span>Target Space</span><strong id="selectedTargetSpace">β€”</strong></div>
147
+ <div><span>Hardware</span><strong id="selectedHardware">β€”</strong></div>
148
+ <div><span>Model</span><strong id="selectedModelShort">β€”</strong></div>
149
+ <div><span>Mode</span><strong id="selectedMode">Gradio (Python)</strong></div>
150
+ </div>
151
+ <div class="quick-links selected-links-row">
152
+ <a class="link-btn disabled" id="selectedOpenSpace" target="_blank">Space β†—</a>
153
+ <a class="link-btn disabled" id="selectedOpenJob" target="_blank">Job β†—</a>
154
+ <a class="link-btn disabled" id="selectedOpenArtifacts" target="_blank">Artifacts β†—</a>
155
+ <a class="link-btn disabled" id="selectedOpenSettings" target="_blank">Settings β†—</a>
156
+ </div>
157
+ <div class="selected-actions-stack">
158
+ <button class="secondary full" id="selectedValidateBtn" disabled>Prefill Space Test</button>
159
+ <button class="secondary danger full" id="deleteSelectedRun" disabled>Delete run from bucket</button>
160
+ </div>
161
+ <div class="blockers" id="selectedBlockers" hidden></div>
162
+ </section>
163
+ </section>
164
+ </section>
165
+
166
+ <section class="panel space-test-panel" id="spaceTestSection">
167
+ <div class="space-test-grid">
168
+ <div>
169
+ <div class="section-head compact"><div><span class="panel-icon">βš—</span><h2>Space Test</h2></div></div>
170
+ <div class="form-grid test-top-row">
171
+ <label>Target Space ID<input id="validateSpace" placeholder="fffiloni/my-llama-31-8b-demo" /></label>
172
+ <label>API name<input id="validateApi" value="/predict" /></label>
173
+ <label>Expected output type<select id="expectedOutput"><option value="text">Text</option><option value="image">Image</option><option value="video">Video</option><option value="audio">Audio</option><option value="any">Any</option></select></label>
174
+ <label>Timeout (s)<input id="validateTimeout" value="60" /></label>
175
+ </div>
176
+ <div class="editor-grid">
177
+ <label>Test args (JSON)<textarea id="testArgs" rows="7">[
178
+ {
179
+ "inputs": "Explain the concept of agentic AI in one paragraph."
180
+ }
181
+ ]</textarea></label>
182
+ <label>Test kwargs (JSON)<textarea id="testKwargs" rows="7">{
183
+ "max_new_tokens": 128,
184
+ "temperature": 0.2
185
+ }</textarea></label>
186
+ </div>
187
+ </div>
188
+
189
+ <div class="validation-result-card" id="spaceTestPreview">
190
+ <div class="validation-grid">
191
+ <div><span>Validation status</span><strong><span class="badge neutral" id="spaceTestStatus">Pending</span></strong></div>
192
+ <div><span>Latency</span><strong>β€”</strong></div>
193
+ <div><span>Validation run ID</span><strong id="spaceTestOutput">β€”</strong></div>
194
+ <div><span>Started at</span><strong id="lastPolled">β€”</strong></div>
195
+ <div><span>Verdict</span><strong id="spaceTestVerdict">pending</strong></div>
196
+ </div>
197
+ <button class="primary full" id="launchValidate">Launch validation</button>
198
+ </div>
199
+ </div>
200
+ </section>
201
+
202
+ <div class="support-elements" hidden>
203
+ <input id="runSearch" />
204
+ <input id="runId" />
205
+ <textarea id="buildGoal"></textarea>
206
+ <button id="refreshRuns"></button>
207
+ <div id="runsTableHome"></div>
208
+ <button id="refreshRunsHome"></button>
209
+ <button id="resumeLatestRunFull"></button>
210
+ <a id="runHeaderSpace" target="_blank"></a>
211
+
212
+ <span id="progressTitle"></span><span id="lastEvent"></span><span id="hardwarePlan"></span>
213
+ <span id="detailJobStatus"></span><span id="detailGateStatus"></span><span id="detailHardware"></span><span id="detailTraces"></span><span id="homeRunDetailStatus"></span>
214
+ <span id="selectedModelIdHidden"></span><span id="selectedHardwareHidden"></span>
215
+ <ul id="selectedValidation"></ul><ul id="zeroGpuChecklist"></ul><div id="statusChips"></div>
216
+ <div id="diagBuildStatus"></div><div id="diagBuildStep"></div>
217
+ <div id="issueCard"><strong id="issueTitle"></strong><p id="issueDetail"></p></div>
218
+ <ul id="metricsList"></ul>
219
+ <pre id="reportPreview"></pre><div id="artifactList"></div>
220
+ </div>
221
+ </main>
222
+ </div>
223
+ <script src="/custom-static/api.js"></script>
224
+ <script src="/custom-static/components.js"></script>
225
+ <script src="/custom-static/progress.js"></script>
226
+ <script src="/custom-static/runs.js"></script>
227
+ <script src="/custom-static/app.js"></script>
228
+ </body>
229
+ </html>
web/static/progress.js CHANGED
@@ -48,6 +48,36 @@ function visualStatusClass(status){
48
  if(s.includes('running')||s.includes('queued')||s.includes('pending')||s.includes('waiting')||s.includes('building'))return'running';
49
  return'neutral';
50
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  function stepOrderIndex(step){
52
  const idx=WORKER_STEP_ORDER.indexOf(String(step||''));
53
  return idx<0?-1:idx;
@@ -62,6 +92,7 @@ function timelineIcon(status){
62
  if(status==='done'||status==='completed')return'βœ“';
63
  if(status==='running')return'β€’';
64
  if(status==='failed')return'!';
 
65
  if(status==='blocked'||status==='cancelled'||status==='stopped')return'β– ';
66
  if(status==='skipped')return'β†·';
67
  return'β—‹';
@@ -71,6 +102,7 @@ function timelineStatus(status,overallStatus=''){
71
  const overall=normalizeTimelineOverallStatus(overallStatus);
72
  const stopped=overall&&PROGRESS_TERMINAL_STATUSES.has(overall)&&!['success','succeeded','done','full_inference_success','full_inference_candidate_health_passed'].includes(overall);
73
  if(status==='failed'||status==='error'||status==='failure')return'status-failed';
 
74
  if(status==='blocked'||status==='cancelled'||status==='canceled'||status==='stopped')return'status-stopped';
75
  if(status==='done'||status==='completed'||status==='success')return'status-done';
76
  if(status==='running'||status==='waiting')return stopped?'status-stopped':'status-running';
@@ -140,18 +172,21 @@ function ensureTimelineStepVisible(stepKey){
140
  }
141
  root.dataset.visibleStep=stepKey;
142
  }
143
- function compactTimelineItems(items=[]){
144
  const byStep=new Map((items||[]).map(item=>[String(item.step||item.id||item.label||''),item]));
145
- const terminalRaw=new Set(['failed','blocked','stopped','cancelled','canceled']);
146
  return COMPACT_TIMELINE_GROUPS.map(group=>{
147
  const groupItems=group.steps.map(step=>byStep.get(step)).filter(Boolean);
148
  const doneCount=groupItems.filter(item=>['done','completed','success'].includes(String(item.status||'').toLowerCase())).length;
149
  const active=groupItems.find(item=>['running','waiting','stopped','failed','blocked','cancelled','canceled'].includes(String(item.status||'').toLowerCase()));
150
  let status='pending';
151
- if(active)status=String(active.status||'running').toLowerCase();
 
 
152
  else if(groupItems.length&&doneCount>=group.steps.length)status='done';
153
  else if(doneCount>0)status='running';
154
- const activeSubstep=active?.step||active?.id||active?.label||groupItems[groupItems.length-1]?.step||'';
 
155
  const activeIndex=activeSubstep?Math.max(0,group.steps.indexOf(activeSubstep)):-1;
156
  let percent=0;
157
  if(status==='done')percent=100;
@@ -181,7 +216,7 @@ function renderTimeline(items,overallStatus=''){
181
  if(!root)return;
182
  if(!root.dataset.scrollBound){root.dataset.scrollBound='1';root.addEventListener('scroll',()=>{root.dataset.userScrolled='1'},{passive:true});}
183
  const fullList=items||[];
184
- const list=compactTimelineItems(fullList);
185
  const currentKey=currentCompactGroupKey(fullList);
186
  const signature=list.map((x,i)=>`${timelineStepKey(x,i)}:${timelineStatus(x.status,overallStatus)}:${x.label||''}:${overallStatus||''}`).join('|');
187
  if(root.dataset.signature===signature){ensureTimelineStepVisible(currentKey);return;}
 
48
  if(s.includes('running')||s.includes('queued')||s.includes('pending')||s.includes('waiting')||s.includes('building'))return'running';
49
  return'neutral';
50
  }
51
+ function groupForWorkerStep(step){
52
+ return COMPACT_TIMELINE_GROUPS.find(g=>g.steps.includes(String(step||'')))||null;
53
+ }
54
+ function latestEventIndexForGroup(group){
55
+ if(!group)return -1;
56
+ let idx=-1;
57
+ (activityFeedState.lastItems||[]).forEach((event,i)=>{
58
+ if(group.steps.includes(String(event.step||'')))idx=i;
59
+ });
60
+ return idx;
61
+ }
62
+ function failedEventInfoForGroup(group){
63
+ if(!group)return null;
64
+ let info=null;
65
+ (activityFeedState.lastItems||[]).forEach((event,i)=>{
66
+ if(group.steps.includes(String(event.step||''))&&isFailureStatus(event.status)){
67
+ info={event,index:i};
68
+ }
69
+ });
70
+ return info;
71
+ }
72
+ function failedTimelineStatusForGroup(group,overallStatus){
73
+ const failed=failedEventInfoForGroup(group);
74
+ if(!failed)return '';
75
+ const latestIdx=latestEventIndexForGroup(group);
76
+ const overall=visualStatusClass(overallStatus);
77
+ const superseded=latestIdx>failed.index;
78
+ if(overall==='error')return 'failed';
79
+ return superseded?'recovered':'failed';
80
+ }
81
  function stepOrderIndex(step){
82
  const idx=WORKER_STEP_ORDER.indexOf(String(step||''));
83
  return idx<0?-1:idx;
 
92
  if(status==='done'||status==='completed')return'βœ“';
93
  if(status==='running')return'β€’';
94
  if(status==='failed')return'!';
95
+ if(status==='recovered'||status==='superseded')return'β†Ί';
96
  if(status==='blocked'||status==='cancelled'||status==='stopped')return'β– ';
97
  if(status==='skipped')return'β†·';
98
  return'β—‹';
 
102
  const overall=normalizeTimelineOverallStatus(overallStatus);
103
  const stopped=overall&&PROGRESS_TERMINAL_STATUSES.has(overall)&&!['success','succeeded','done','full_inference_success','full_inference_candidate_health_passed'].includes(overall);
104
  if(status==='failed'||status==='error'||status==='failure')return'status-failed';
105
+ if(status==='recovered'||status==='superseded')return'status-warn';
106
  if(status==='blocked'||status==='cancelled'||status==='canceled'||status==='stopped')return'status-stopped';
107
  if(status==='done'||status==='completed'||status==='success')return'status-done';
108
  if(status==='running'||status==='waiting')return stopped?'status-stopped':'status-running';
 
172
  }
173
  root.dataset.visibleStep=stepKey;
174
  }
175
+ function compactTimelineItems(items=[],overallStatus=''){
176
  const byStep=new Map((items||[]).map(item=>[String(item.step||item.id||item.label||''),item]));
177
+ const terminalRaw=new Set(['failed','blocked','stopped','cancelled','canceled','recovered']);
178
  return COMPACT_TIMELINE_GROUPS.map(group=>{
179
  const groupItems=group.steps.map(step=>byStep.get(step)).filter(Boolean);
180
  const doneCount=groupItems.filter(item=>['done','completed','success'].includes(String(item.status||'').toLowerCase())).length;
181
  const active=groupItems.find(item=>['running','waiting','stopped','failed','blocked','cancelled','canceled'].includes(String(item.status||'').toLowerCase()));
182
  let status='pending';
183
+ const historicalFailure=failedTimelineStatusForGroup(group,overallStatus);
184
+ if(historicalFailure)status=historicalFailure;
185
+ else if(active)status=String(active.status||'running').toLowerCase();
186
  else if(groupItems.length&&doneCount>=group.steps.length)status='done';
187
  else if(doneCount>0)status='running';
188
+ const failedInfo=failedEventInfoForGroup(group);
189
+ const activeSubstep=failedInfo&&historicalFailure?(failedInfo.event.step||failedInfo.event.message||'failed'):active?.step||active?.id||active?.label||groupItems[groupItems.length-1]?.step||'';
190
  const activeIndex=activeSubstep?Math.max(0,group.steps.indexOf(activeSubstep)):-1;
191
  let percent=0;
192
  if(status==='done')percent=100;
 
216
  if(!root)return;
217
  if(!root.dataset.scrollBound){root.dataset.scrollBound='1';root.addEventListener('scroll',()=>{root.dataset.userScrolled='1'},{passive:true});}
218
  const fullList=items||[];
219
+ const list=compactTimelineItems(fullList,overallStatus);
220
  const currentKey=currentCompactGroupKey(fullList);
221
  const signature=list.map((x,i)=>`${timelineStepKey(x,i)}:${timelineStatus(x.status,overallStatus)}:${x.label||''}:${overallStatus||''}`).join('|');
222
  if(root.dataset.signature===signature){ensureTimelineStepVisible(currentKey);return;}
web/static/runs.js CHANGED
@@ -23,7 +23,7 @@ function renderRunRows(runs,compact=false){return (runs||[]).map(r=>`<button cla
23
  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()}))}
24
  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);document.querySelectorAll('.run-row[data-run]').forEach(row=>row.addEventListener('click',()=>selectRun(row.dataset.run)));if(state.runId)document.querySelectorAll('.run-row').forEach(row=>row.classList.toggle('selected',row.dataset.run===state.runId))}
25
  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'))}}
26
- async function selectRun(runId){if(!runId)return;document.querySelectorAll('.run-row').forEach(row=>row.classList.toggle('selected',row.dataset.run===runId));setBuildModeInspecting(runId);try{const detail=await apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}`);if(detail.bucket_source)state.bucketSource=detail.bucket_source;renderRunDetail(detail);startPolling(runId);/* Keep the user on the current tab: selecting a run updates the side/detail panels without forcing navigation. */}catch(e){const report=$('reportPreview');if(report)report.textContent='Could not load run details: '+e.message;const home=$('homeReportPreview');if(home)home.innerHTML=`<strong>Could not load run details</strong><p>${escapeHtml(e.message)}</p>`}}
27
  async function deleteSelectedRun(){
28
  const detail=state.selectedRunDetail||{};
29
  const summary=detail.summary||{};
@@ -55,4 +55,4 @@ async function deleteSelectedRun(){
55
  }
56
  function renderSelectedLinks(summary={},detail={}){const runId=summary.run_id||detail.run_id;const bucket=summary.bucket_source||detail.bucket_source||state.bucketSource;const artifacts=summary.artifacts_url||(typeof artifactUrl==='function'?artifactUrl(runId,bucket):'');const jobUrl=inferredJobUrl(summary,detail);setLink('selectedOpenJob',jobUrl);const ready=typeof runPassedSpaceCreation==='function'?runPassedSpaceCreation({summary,detail,events:detail.events||[]}):Boolean(summary.target_space_url);setLink('selectedOpenSpace',ready?summary.target_space_url:'');setLink('selectedOpenSettings',ready&&summary.target_space_url?summary.target_space_url+'/settings':'');setLink('selectedOpenArtifacts',artifacts);const btn=$('selectedValidateBtn');if(btn){btn.disabled=!summary.target_space||!ready;btn.onclick=prepareValidationFromActiveRun}}
57
  function renderBlockers(detail={}){const root=$('selectedBlockers');if(!root)return;const summary=detail.summary||{};const blockers=detail.technical_blockers||{};const manual=summary.manual_hardware_required||String(summary.status||'').includes('manual_hardware_required');const blockerItems=Array.isArray(blockers.blockers)?blockers.blockers:[];if(!manual&&!blockerItems.length){root.hidden=true;root.innerHTML='';return}root.hidden=false;const parts=[];if(manual)parts.push('<strong>Manual hardware required</strong><p>Open Space settings, choose the recommended GPU, then run validation.</p>');for(const b of blockerItems.slice(0,4)){parts.push(`<strong>${escapeHtml(shortText(b.type||b.name||'Technical blocker',32))}</strong><p>${escapeHtml(shortText(b.claim||b.reason||b.message||'',120))}</p>`)}root.innerHTML=parts.join('')}
58
- function renderRunDetail(detail){state.selectedRunDetail=detail;const summary=detail.summary||{};const smoke=detail.generation_smoke||{};const files=detail.files||[];const view=detail.view||{};const header=view.header||{};setText('selectedRunId',detail.run_id||summary.run_id||'β€”');setText('selectedModelId',summary.model_id||'β€”');setText('selectedModelShort',summary.model_id||'β€”');setText('selectedTargetSpace',summary.target_space||'β€”');setText('selectedHardware',summary.selected_hardware||summary.hardware||'β€”');setText('selectedMode',detail.state?.implementation_mode||detail.launch?.implementation_mode||summary.kind||'Strict real inference');setText('runDetailStatus',header.status||summary.status||'unknown');setText('homeRunDetailStatus',header.status_label||summary.status||'unknown');setText('detailRunId',summary.run_id||detail.run_id);setText('detailJobStatus',summary.status||'β€”');setText('detailGateStatus',(detail.inference_gate||{}).status||summary.status||'β€”');setText('detailSpace',summary.target_space||'β€”');setText('detailHardware',summary.selected_hardware||summary.hardware||'β€”');setText('detailTraces',Array.isArray(detail.events)?String(detail.events.length)+' events':summary.traces_count||'β€”');const validation=$('selectedValidation');if(validation){validation.innerHTML=`<li><span>Health</span><strong>${summary.health_passed?'passed':'β€”'}</strong></li><li><span>Smoke test</span><strong>${summary.smoke_test_passed?'passed':(smoke.status||'β€”')}</strong></li><li><span>Latency</span><strong>${summary.latency_seconds?fmtSeconds(Math.round(summary.latency_seconds)):'β€”'}</strong></li>`}const artifactRoot=$('artifactList');if(artifactRoot){const interesting=files.filter(f=>!/traces\//.test(f.path||'')).slice(0,18);artifactRoot.innerHTML=interesting.length?interesting.map(f=>`<a href="${escapeHtml(f.url)}" target="_blank"><span>${escapeHtml(f.path)}</span><em>${f.size?Math.round(Number(f.size)/1024)+' KB':''}</em></a>`).join(''):'No artifacts indexed yet.'}const reportText=(detail.report||'No report available.').slice(0,8000);const report=$('reportPreview');if(report)report.textContent=reportText;const homeReport=$('homeReportPreview');if(homeReport){const firstLine=reportText.split('\n').find(Boolean)||'No report available yet.';homeReport.innerHTML=`<strong>${escapeHtml(header.status_label||summary.status||'Run selected')}</strong><p>${escapeHtml(shortText(reportText,1200))}</p>`}setQuickLinks({job_url:inferredJobUrl(summary,detail),target_space_url:summary.target_space_url,target_space_settings_url:summary.target_space_url?summary.target_space_url+'/settings':'',artifacts_url:summary.artifacts_url,bucket_source:detail.bucket_source,run_id:summary.run_id||detail.run_id});renderSelectedLinks({...summary,bucket_source:detail.bucket_source},detail);renderBlockers(detail);renderMetrics({...detail,summary});/* v71: avoid rendering stale view.pipeline before /progress returns canonical compact timeline. */const deleteBtn=$('deleteSelectedRun');if(deleteBtn){deleteBtn.disabled=!(summary.run_id||detail.run_id);deleteBtn.onclick=deleteSelectedRun}}
 
23
  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()}))}
24
  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);document.querySelectorAll('.run-row[data-run]').forEach(row=>row.addEventListener('click',()=>selectRun(row.dataset.run)));if(state.runId)document.querySelectorAll('.run-row').forEach(row=>row.classList.toggle('selected',row.dataset.run===state.runId))}
25
  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'))}}
26
+ async function selectRun(runId){if(!runId)return;document.querySelectorAll('.run-row').forEach(row=>row.classList.toggle('selected',row.dataset.run===runId));setBuildModeInspecting(runId);try{const detail=await apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}`);if(detail.bucket_source)state.bucketSource=detail.bucket_source;renderRunDetail(detail);startPolling(runId);/* Keep the user on the current tab: selecting a run updates the side/detail panels without forcing navigation. */}catch(e){const report=$('reportPreview');if(report)report.textContent='Could not load run details: '+e.message;}}
27
  async function deleteSelectedRun(){
28
  const detail=state.selectedRunDetail||{};
29
  const summary=detail.summary||{};
 
55
  }
56
  function renderSelectedLinks(summary={},detail={}){const runId=summary.run_id||detail.run_id;const bucket=summary.bucket_source||detail.bucket_source||state.bucketSource;const artifacts=summary.artifacts_url||(typeof artifactUrl==='function'?artifactUrl(runId,bucket):'');const jobUrl=inferredJobUrl(summary,detail);setLink('selectedOpenJob',jobUrl);const ready=typeof runPassedSpaceCreation==='function'?runPassedSpaceCreation({summary,detail,events:detail.events||[]}):Boolean(summary.target_space_url);setLink('selectedOpenSpace',ready?summary.target_space_url:'');setLink('selectedOpenSettings',ready&&summary.target_space_url?summary.target_space_url+'/settings':'');setLink('selectedOpenArtifacts',artifacts);const btn=$('selectedValidateBtn');if(btn){btn.disabled=!summary.target_space||!ready;btn.onclick=prepareValidationFromActiveRun}}
57
  function renderBlockers(detail={}){const root=$('selectedBlockers');if(!root)return;const summary=detail.summary||{};const blockers=detail.technical_blockers||{};const manual=summary.manual_hardware_required||String(summary.status||'').includes('manual_hardware_required');const blockerItems=Array.isArray(blockers.blockers)?blockers.blockers:[];if(!manual&&!blockerItems.length){root.hidden=true;root.innerHTML='';return}root.hidden=false;const parts=[];if(manual)parts.push('<strong>Manual hardware required</strong><p>Open Space settings, choose the recommended GPU, then run validation.</p>');for(const b of blockerItems.slice(0,4)){parts.push(`<strong>${escapeHtml(shortText(b.type||b.name||'Technical blocker',32))}</strong><p>${escapeHtml(shortText(b.claim||b.reason||b.message||'',120))}</p>`)}root.innerHTML=parts.join('')}
58
+ function renderRunDetail(detail){state.selectedRunDetail=detail;const summary=detail.summary||{};const smoke=detail.generation_smoke||{};const files=detail.files||[];const view=detail.view||{};const header=view.header||{};setText('selectedRunId',detail.run_id||summary.run_id||'β€”');setText('selectedModelId',summary.model_id||'β€”');setText('selectedModelShort',summary.model_id||'β€”');setText('selectedTargetSpace',summary.target_space||'β€”');setText('selectedHardware',summary.selected_hardware||summary.hardware||'β€”');setText('selectedMode',detail.state?.implementation_mode||detail.launch?.implementation_mode||summary.kind||'Strict real inference');setText('runDetailStatus',header.status||summary.status||'unknown');setText('homeRunDetailStatus',header.status_label||summary.status||'unknown');setText('detailRunId',summary.run_id||detail.run_id);setText('detailJobStatus',summary.status||'β€”');setText('detailGateStatus',(detail.inference_gate||{}).status||summary.status||'β€”');setText('detailSpace',summary.target_space||'β€”');setText('detailHardware',summary.selected_hardware||summary.hardware||'β€”');setText('detailTraces',Array.isArray(detail.events)?String(detail.events.length)+' events':summary.traces_count||'β€”');const validation=$('selectedValidation');if(validation){validation.innerHTML=`<li><span>Health</span><strong>${summary.health_passed?'passed':'β€”'}</strong></li><li><span>Smoke test</span><strong>${summary.smoke_test_passed?'passed':(smoke.status||'β€”')}</strong></li><li><span>Latency</span><strong>${summary.latency_seconds?fmtSeconds(Math.round(summary.latency_seconds)):'β€”'}</strong></li>`}const artifactRoot=$('artifactList');if(artifactRoot){const interesting=files.filter(f=>!/traces\//.test(f.path||'')).slice(0,18);artifactRoot.innerHTML=interesting.length?interesting.map(f=>`<a href="${escapeHtml(f.url)}" target="_blank"><span>${escapeHtml(f.path)}</span><em>${f.size?Math.round(Number(f.size)/1024)+' KB':''}</em></a>`).join(''):'No artifacts indexed yet.'}const reportText=(detail.report||'No report available.').slice(0,8000);const report=$('reportPreview');if(report)report.textContent=reportText;setQuickLinks({job_url:inferredJobUrl(summary,detail),target_space_url:summary.target_space_url,target_space_settings_url:summary.target_space_url?summary.target_space_url+'/settings':'',artifacts_url:summary.artifacts_url,bucket_source:detail.bucket_source,run_id:summary.run_id||detail.run_id});renderSelectedLinks({...summary,bucket_source:detail.bucket_source},detail);renderBlockers(detail);renderMetrics({...detail,summary});/* v71: avoid rendering stale view.pipeline before /progress returns canonical compact timeline. */const deleteBtn=$('deleteSelectedRun');if(deleteBtn){deleteBtn.disabled=!(summary.run_id||detail.run_id);deleteBtn.onclick=deleteSelectedRun}}