fffiloni commited on
Commit
8be4404
Β·
verified Β·
1 Parent(s): b23082b

Upload 6 files

Browse files
web/static/app.css CHANGED
@@ -1,22 +1,13 @@
1
  :root{
2
- --bg:#f6f8fb;--panel:#fff;--panel2:#fbfcff;--ink:#0f172a;--muted:#64748b;--soft:#94a3b8;--line:#e4e8f0;
3
- --blue:#2563eb;--blue2:#1d4ed8;--blue-soft:#eef5ff;--green:#16a34a;--green-soft:#ecfdf3;--amber:#d97706;--amber-soft:#fff7e6;--red:#dc2626;--red-soft:#fff1f2;
4
- --radius:18px;--radius-sm:12px;--shadow:0 18px 42px rgba(15,23,42,.07);--shadow-sm:0 10px 24px rgba(15,23,42,.055);
5
  }
6
- *{box-sizing:border-box}
7
- html{background:var(--bg)}
8
- body{margin:0;background:radial-gradient(circle at top left,#eef5ff 0,#f6f8fb 32%,#f4f7fb 100%);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased}
9
- .app-shell{min-height:100vh;max-width:1540px;margin:0 auto;padding:24px 28px 38px}
10
- .app-header{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin-bottom:18px}
11
- .brand-lockup{display:flex;gap:13px;align-items:center}.brand-icon{width:44px;height:44px;border-radius:15px;background:linear-gradient(135deg,var(--blue),#7c3aed);color:#fff;display:grid;place-items:center;box-shadow:0 8px 24px rgba(37,99,235,.24)}
12
- .brand-lockup h1{margin:0;font-size:25px;letter-spacing:-.035em}.brand-lockup p{margin:4px 0 0;color:var(--muted);font-size:14px}.top-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:flex-end}.bucket-pill,.user-chip,.logout-link{border:1px solid var(--line);background:rgba(255,255,255,.92);padding:8px 12px;border-radius:999px;font-size:13px;display:flex;gap:8px;align-items:center;white-space:nowrap;box-shadow:0 5px 16px rgba(15,23,42,.035)}.logout-link{color:#334155;font-weight:800;text-decoration:none}.logout-link:hover{background:#f8fafc;transform:translateY(-1px)}
13
- .status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;background:#94a3b8}.green{background:var(--green)}.amber-dot{background:var(--amber)}.red-dot{background:var(--red)}
14
- .top-tabs{display:flex;gap:8px;align-items:center;background:rgba(255,255,255,.78);border:1px solid var(--line);box-shadow:var(--shadow-sm);border-radius:18px;padding:6px;margin-bottom:18px;width:max-content;max-width:100%;overflow:auto}.nav-item{border:0;background:transparent;padding:11px 16px;border-radius:13px;color:#334155;font-weight:850;cursor:pointer;white-space:nowrap;transition:.16s ease}.nav-item:hover{background:#f1f5f9}.nav-item.active{background:var(--blue);color:#fff;box-shadow:0 9px 22px rgba(37,99,235,.22)}
15
- .main{min-width:0}.auth-panel,.operation-status{border:1px solid var(--line);border-radius:14px;padding:11px 14px;margin-bottom:16px;background:#fff;color:#334155;box-shadow:0 8px 20px rgba(15,23,42,.04);font-size:14px}.auth-panel.ok{display:none}.auth-panel.warning,.operation-status.warning{border-color:#f59e0b;background:var(--amber-soft);color:#92400e}.operation-status.loading{border-color:#bfdbfe;background:#eff6ff;color:#1d4ed8}.operation-status.success{border-color:rgba(22,163,74,.25);background:var(--green-soft);color:#166534}.operation-status.error{border-color:#fecaca;background:#fef2f2;color:#991b1b}.auth-panel p{margin:4px 0 10px;color:inherit;font-size:13px}
16
- .single-column{max-width:1020px}.dashboard-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(360px,430px);gap:18px;align-items:start}.workspace-column{display:flex;flex-direction:column;gap:16px}.right-rail{display:flex;flex-direction:column;gap:16px;min-width:0}.panel,.progress-card,.bucket-strip{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:18px}.bucket-strip{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:16px}.eyebrow{display:block;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em;font-weight:850;margin-bottom:4px}.bucket-source{display:flex;gap:9px;align-items:center}.bucket-source strong{font-size:15px}.mini-copy{margin:8px 0 0}.badge{font-size:12px;border-radius:999px;padding:4px 10px;margin-left:8px;display:inline-flex;align-items:center;gap:5px;font-weight:850;background:#f1f5f9;color:#475569}.badge.success,.running,.live{background:var(--green-soft);color:#15803d}.badge.warn{background:var(--amber-soft);color:#92400e}.badge.error{background:var(--red-soft);color:#991b1b}.badge.neutral{background:#eef2ff;color:#3730a3}.strip-actions{display:flex;gap:8px;flex-wrap:wrap}.hero-card{max-width:1000px}.panel h2,.progress-card h2{font-size:18px;letter-spacing:-.02em;margin:0 0 12px}.panel-title-row,.panel-head,.progress-head{display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap}.form-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:13px}.compact-form{margin-top:14px}label{font-size:13px;font-weight:750;color:#334155}input,select,textarea{width:100%;margin-top:6px;border:1px solid var(--line);background:#fff;border-radius:11px;padding:11px 12px;font:inherit;color:var(--ink);outline:none;transition:border .15s ease,box-shadow .15s ease}input:focus,select:focus,textarea:focus{border-color:#bfdbfe;box-shadow:0 0 0 4px #dbeafe}textarea{resize:vertical;min-height:100px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-size:13px;line-height:1.45}.toggles{display:grid;gap:9px;margin:15px 0}.toggles label{border:1px solid var(--line);border-radius:13px;padding:10px 12px;background:#fafcff;display:flex;gap:10px;align-items:flex-start}.toggles input{width:auto;margin:2px 0 0}.toggles small{display:block;color:var(--muted);font-weight:500;margin-top:2px}.primary,.secondary,.link-btn{border-radius:11px;padding:10px 13px;font-weight:850;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:.15s ease}.primary{border:0;background:linear-gradient(135deg,var(--blue),var(--blue2));color:white;width:100%;font-size:15px;box-shadow:0 10px 24px rgba(37,99,235,.25)}.primary:hover,.secondary:hover,.link-btn:hover{transform:translateY(-1px)}.primary:disabled,.secondary:disabled{opacity:.5;cursor:not-allowed;transform:none}.secondary{border:1px solid var(--line);background:#fff;color:#334155}.secondary.small{padding:8px 10px;font-size:12px}.secondary.full{width:100%;margin-top:12px}.link-btn{border:1px solid var(--line);color:#1f2937;background:#fff;font-size:13px}.link-btn.disabled{opacity:.45;pointer-events:none}.build-gate{margin:10px 0 0;text-align:center}.is-busy::after{content:"";width:12px;height:12px;border:2px solid currentColor;border-right-color:transparent;border-radius:999px;display:inline-block;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}
17
- .build-mode-panel{border:1px solid #dbeafe;background:linear-gradient(180deg,#fff,#f8fbff);border-radius:14px;padding:12px 13px;margin:12px 0}.build-mode-panel strong{color:#1d4ed8}.build-mode-panel p{margin:5px 0 0;color:var(--muted);font-size:13px}.build-mode-panel.inspecting{border-color:#f59e0b;background:var(--amber-soft)}.build-mode-panel.inspecting strong{color:#92400e}.build-mode-panel code{background:rgba(255,255,255,.65);border:1px solid #fde68a;border-radius:6px;padding:1px 4px}
18
- .progress-card{margin:0}.progress-head strong:first-child{font-size:18px}.progress-head>strong{font-size:24px;letter-spacing:-.04em}.progress-bar{height:10px;background:#e6eaf1;border-radius:999px;margin:14px 0;overflow:hidden}.progress-bar div{height:100%;background:linear-gradient(90deg,var(--blue),#7c3aed);border-radius:999px}.progress-meta{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px;border-top:1px solid var(--line);padding-top:13px}.progress-meta div{min-width:0;background:#fbfdff;border:1px solid var(--line);border-radius:12px;padding:10px}.progress-meta span,.detail-grid span{display:block;color:var(--muted);font-size:12px;margin-bottom:4px}.progress-meta strong,.detail-grid strong{overflow-wrap:anywhere;font-size:13px}.timeline{display:grid;grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:8px;margin-top:16px;overflow:visible}.step{display:grid;grid-template-columns:26px minmax(0,1fr);gap:8px;align-items:center;padding:9px 10px;border:1px solid var(--line);border-radius:12px;background:#fbfdff;color:#475569;font-size:12px;min-height:46px}.step-dot{width:24px;height:24px;border-radius:50%;display:grid;place-items:center;font-size:12px;font-weight:900;border:1px solid #cbd5e1;background:#fff;color:#64748b}.step div:last-child{font-weight:750}.step::after{content:attr(data-status);grid-column:2;text-transform:uppercase;font-size:9px;letter-spacing:.06em;color:#64748b;font-weight:850;margin-top:-5px}.step.done{background:#f0fdf4;border-color:#bbf7d0}.step.done .step-dot{background:#dcfce7;border-color:var(--green);color:var(--green)}.step.running{background:#eff6ff;border-color:#bfdbfe}.step.running .step-dot{background:#dbeafe;border-color:var(--blue);color:var(--blue);box-shadow:0 0 0 3px #dbeafe}.step.failed{background:#fff1f2;border-color:#fecaca}.step.failed .step-dot{background:#fee2e2;border-color:var(--red);color:var(--red)}.step.pending{opacity:.78}.manual-action{margin-top:14px;border:1px solid #f59e0b;background:var(--amber-soft);border-radius:14px;padding:13px;display:flex;justify-content:space-between;gap:12px;align-items:center}.manual-action p{margin:4px 0 0;color:#92400e;font-size:13px}.manual-action strong{color:#92400e}.manual-action-buttons{display:flex;gap:8px;flex-wrap:wrap}.poll-warning{margin-top:12px;border:1px solid #f59e0b;background:var(--amber-soft);color:#92400e;border-radius:12px;padding:10px 12px;font-size:13px}.bottom-grid{display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);gap:16px}.metric-list{list-style:none;margin:0;padding:0}.metric-list li{display:flex;justify-content:space-between;border-bottom:1px solid var(--line);padding:9px 0;gap:12px}.metric-list li:last-child{border-bottom:0}.metric-ok{color:var(--green)}.metric-warn{color:var(--amber)}.events{margin:0;background:#0f172a;border:1px solid #1e293b;border-radius:13px;padding:12px;min-height:160px;max-height:280px;overflow:auto;font-size:12px;color:#dbeafe;white-space:pre-wrap;line-height:1.5}
19
- .run-filters{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0 12px}.filter{border:1px solid var(--line);background:#fff;color:#334155;border-radius:999px;padding:7px 10px;font-weight:800;font-size:12px;cursor:pointer}.filter.active{background:#eaf2ff;border-color:#bfdbfe;color:#1d4ed8}.runs-table{border:1px solid var(--line);border-radius:14px;padding:0;overflow:hidden;color:var(--muted);background:#fff}.run-explorer.is-refreshing .runs-table{opacity:.65}.run-row{width:100%;display:grid;grid-template-columns:14px minmax(110px,1fr) minmax(95px,.9fr) auto minmax(90px,.8fr) 74px;gap:8px;align-items:center;border:0;border-bottom:1px solid var(--line);background:#fff;text-align:left;padding:10px 11px;cursor:pointer;color:var(--ink);font:inherit}.run-row:last-child{border-bottom:0}.run-row:hover{background:#f8fafc}.run-row.selected{background:#eef2ff}.run-row.selected strong{color:#1d4ed8}.run-id{font-size:12px}.dot{width:8px;height:8px;border-radius:999px;background:#94a3b8}.dot.success{background:var(--green)}.dot.running{background:var(--blue)}.dot.warn{background:var(--amber)}.dot.error{background:var(--red)}.pill{font-style:normal;font-size:10px;border-radius:999px;padding:4px 7px;background:#f1f5f9;color:#475569;white-space:nowrap;font-weight:850}.pill.success{background:#dcfce7;color:#166534}.pill.running{background:#dbeafe;color:#1d4ed8}.pill.warn{background:#fef3c7;color:#92400e}.pill.error{background:#fee2e2;color:#991b1b}.tiny-note{font-size:12px;margin:10px 0 0}.detail-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px}.detail-grid div{border:1px solid var(--line);border-radius:12px;padding:10px;min-width:0;background:#fbfdff}.home-detail-grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.quick-links{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:13px}.report-home{border:1px solid var(--line);border-radius:13px;background:#fbfdff;padding:12px;margin-top:13px}.report-home strong{display:flex;gap:8px;align-items:center}.report-home p{margin:6px 0 0;color:var(--muted);font-size:13px;line-height:1.45}.test-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(300px,360px);gap:18px}.large{max-width:900px}.help-panel{align-self:start}.blockers{border:1px solid #f59e0b;background:var(--amber-soft);border-radius:12px;padding:12px;margin:12px 0}.blockers strong{display:block;color:#92400e;font-size:13px}.blockers p{margin:4px 0 10px;color:#92400e;font-size:12px}.hidden-run-inspector{display:none!important}
20
- .empty{padding:16px;color:var(--muted)}[hidden]{display:none!important}
21
- @media(max-width:1280px){.dashboard-grid{grid-template-columns:1fr}.right-rail{display:grid;grid-template-columns:1fr 1fr}.progress-meta{grid-template-columns:repeat(2,minmax(0,1fr))}.test-layout{grid-template-columns:1fr}.single-column{max-width:none}}
22
- @media(max-width:860px){.app-shell{padding:16px}.app-header{display:block}.top-actions{justify-content:flex-start;margin-top:12px}.top-tabs{width:100%}.nav-item{flex:1}.bucket-strip,.manual-action{display:block}.strip-actions,.manual-action-buttons{margin-top:12px}.form-grid,.bottom-grid,.right-rail,.detail-grid,.home-detail-grid,.quick-links,.progress-meta{grid-template-columns:1fr}.timeline{grid-template-columns:1fr 1fr}.run-row{grid-template-columns:14px minmax(0,1fr);gap:7px}.run-row span:nth-child(n+3),.run-row em{grid-column:2}.toggles{grid-template-columns:1fr}}
 
1
  :root{
2
+ --bg:#f7f9fc;--panel:#fff;--panel2:#fbfdff;--ink:#101828;--muted:#667085;--soft:#98a2b3;--line:#e4e7ec;
3
+ --blue:#1f6feb;--blue2:#0f5bd7;--blue-soft:#eff6ff;--green:#16a34a;--green-soft:#ecfdf3;--amber:#f59e0b;--amber-soft:#fffbeb;--red:#ef4444;--red-soft:#fef2f2;--purple:#7c3aed;
4
+ --radius:18px;--radius-sm:12px;--shadow:0 14px 36px rgba(16,24,40,.07);--shadow-sm:0 8px 22px rgba(16,24,40,.055)
5
  }
6
+ *{box-sizing:border-box}html,body{min-height:100%;background:var(--bg)}body{margin:0;color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased;background:radial-gradient(circle at top,#fff 0,#f7f9fc 42%,#f3f6fb 100%)}
7
+ .app-shell{min-height:100vh;max-width:1540px;margin:0 auto;padding:18px 22px 28px}.topbar{height:auto;display:grid;grid-template-columns:minmax(260px,1fr) auto minmax(320px,1fr);gap:18px;align-items:center;border:1px solid var(--line);background:rgba(255,255,255,.9);box-shadow:var(--shadow-sm);border-radius:22px;padding:12px 16px;margin-bottom:18px;backdrop-filter:blur(10px)}
8
+ .brand{display:flex;align-items:center;gap:12px}.brand-mark{width:38px;height:38px;border-radius:12px;display:grid;place-items:center;background:linear-gradient(135deg,var(--blue),var(--purple));color:#fff;font-size:22px;box-shadow:0 10px 24px rgba(31,111,235,.22)}.brand h1{font-size:22px;letter-spacing:-.035em;margin:0}.brand p{margin:2px 0 0;color:var(--muted);font-size:13px}.top-tabs{display:flex;align-items:center;gap:4px;background:#f2f4f7;border:1px solid var(--line);border-radius:15px;padding:4px}.nav-item{border:0;background:transparent;padding:10px 18px;border-radius:11px;color:#344054;font-weight:800;cursor:pointer;white-space:nowrap}.nav-item:hover{background:#fff}.nav-item.active{background:#fff;color:var(--blue);box-shadow:0 5px 15px rgba(16,24,40,.08)}.top-actions{display:flex;justify-content:flex-end;gap:10px;align-items:center}.bucket-pill,.user-chip,.logout-link{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--line);background:#fff;border-radius:999px;padding:9px 12px;color:#344054;font-size:13px;text-decoration:none;font-weight:700}.bucket-pill{cursor:pointer}.status-dot{width:8px;height:8px;border-radius:999px;background:#98a2b3;display:inline-block}.green{background:var(--green)}.amber-dot{background:var(--amber)}.red-dot{background:var(--red)}
9
+ .main{min-width:0}.auth-panel,.operation-status{border:1px solid var(--line);border-radius:14px;padding:12px 14px;margin-bottom:14px;background:#fff;color:#344054;box-shadow:var(--shadow-sm);font-size:14px}.auth-panel.warning,.operation-status.warning{border-color:#fbbf24;background:var(--amber-soft);color:#92400e}.operation-status.loading{border-color:#bfdbfe;background:var(--blue-soft);color:#1d4ed8}.operation-status.success{border-color:#bbf7d0;background:var(--green-soft);color:#166534}.operation-status.error{border-color:#fecaca;background:var(--red-soft);color:#991b1b}.auth-panel p{margin:4px 0 10px;color:inherit}.panel{background:var(--panel);border:1px solid var(--line);box-shadow:var(--shadow);border-radius:var(--radius);padding:20px}.build-workspace{display:grid;grid-template-columns:minmax(390px,520px) minmax(0,1fr);gap:18px;align-items:start}.build-column,.runs-column{min-width:0}.section-head{display:flex;justify-content:space-between;align-items:flex-start;gap:14px;margin-bottom:16px}.section-head.compact{align-items:center}.eyebrow{display:block;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em;font-weight:900;margin-bottom:5px}h2{font-size:22px;line-height:1.15;letter-spacing:-.035em;margin:0}h3{font-size:18px;letter-spacing:-.02em;margin:0 0 8px}h4{font-size:14px;margin:0 0 12px;color:#1d2939}.muted{color:var(--muted);font-size:13px;line-height:1.45}.field-stack{display:grid;gap:13px}label{font-size:13px;font-weight:780;color:#344054}input,select,textarea{width:100%;margin-top:7px;border:1px solid var(--line);background:#fff;border-radius:11px;padding:11px 12px;font:inherit;color:var(--ink);outline:none;transition:border .15s ease,box-shadow .15s ease}textarea{resize:vertical;min-height:90px;font-family:inherit}input:focus,select:focus,textarea:focus{border-color:#93c5fd;box-shadow:0 0 0 4px #dbeafe}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.settings-box{border:1px solid var(--line);border-radius:15px;padding:12px;margin:16px 0;background:#fcfcfd}.settings-box summary{font-weight:850;cursor:pointer;color:#344054;margin-bottom:12px}.toggle-list{display:grid;gap:8px;margin-top:12px}.toggle-list label{display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--line);padding-top:9px}.toggle-list small{display:block;color:var(--muted);font-weight:500;margin-top:2px}.toggle-list input{width:auto;margin:0;accent-color:var(--blue)}.primary,.secondary,.link-btn,.text-link{border-radius:11px;padding:10px 13px;font-weight:850;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:.15s ease}.primary{border:0;background:linear-gradient(135deg,var(--blue),var(--blue2));color:white;width:100%;font-size:15px;box-shadow:0 10px 24px rgba(31,111,235,.25)}.secondary{border:1px solid var(--line);background:#fff;color:#344054}.secondary.small{padding:8px 10px;font-size:12px}.secondary.full{width:100%;margin-top:12px}.link-btn{border:1px solid var(--line);background:#fff;color:#344054;font-size:13px}.link-btn.disabled{opacity:.45;pointer-events:none}.text-link{border:0;background:transparent;color:var(--blue);padding:10px 0;justify-content:flex-start}.primary:hover,.secondary:hover,.link-btn:hover{transform:translateY(-1px)}.primary:disabled,.secondary:disabled{opacity:.5;cursor:not-allowed;transform:none}.is-busy::after{content:"";width:12px;height:12px;border:2px solid currentColor;border-right-color:transparent;border-radius:999px;display:inline-block;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}
10
+ .build-mode-panel{border:1px solid #bfdbfe;background:linear-gradient(180deg,#fff,#f8fbff);border-radius:14px;padding:12px;margin-bottom:14px}.build-mode-panel strong{color:#1d4ed8}.build-mode-panel p{margin:5px 0 0;color:var(--muted);font-size:13px}.build-mode-panel.inspecting{border-color:#f59e0b;background:var(--amber-soft)}.build-mode-panel.inspecting strong{color:#92400e}.build-mode-panel code{background:rgba(255,255,255,.8);border:1px solid #fde68a;border-radius:6px;padding:1px 4px}.run-storage-card{display:grid;grid-template-columns:1fr auto auto;gap:12px;align-items:center;border:1px solid var(--line);border-radius:15px;padding:13px;background:#fff}.run-storage-card strong{display:flex;align-items:center;gap:8px}.inline-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.badge{font-size:12px;border-radius:999px;padding:5px 10px;display:inline-flex;align-items:center;gap:5px;font-weight:850;background:#f2f4f7;color:#475467}.badge.success,.pill.success,.status-chip.success{background:var(--green-soft);color:#15803d}.badge.warn,.pill.warn,.status-chip.warn{background:var(--amber-soft);color:#92400e}.badge.error,.pill.error,.status-chip.error{background:var(--red-soft);color:#991b1b}.badge.running,.pill.running,.status-chip.running{background:var(--blue-soft);color:#1d4ed8}.badge.neutral,.pill.neutral,.status-chip.neutral{background:#f2f4f7;color:#475467}.launch-summary{border:1px solid var(--line);border-radius:15px;padding:13px;margin:14px 0;background:#fbfdff}.launch-summary ul{margin:8px 0 0;padding:0;list-style:none}.launch-summary li{font-size:13px;color:#344054;margin:6px 0}.launch-summary li:before{content:"βœ“";color:var(--green);font-weight:900;margin-right:8px}.build-gate{text-align:center;margin:9px 0 0}
11
+ .runs-layout{display:grid;grid-template-columns:300px minmax(0,1fr);gap:16px}.run-list-card{border:1px solid var(--line);border-radius:16px;padding:12px;background:#fbfdff;min-width:0}.search-wrap{position:relative;margin-bottom:10px}.search-wrap input{padding-right:36px;margin:0}.search-wrap span{position:absolute;right:12px;top:50%;transform:translateY(-50%);color:#667085}.run-filters{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}.filter{border:1px solid var(--line);background:#fff;color:#344054;border-radius:999px;padding:7px 10px;font-weight:800;font-size:12px;cursor:pointer}.filter.active{background:#eaf2ff;border-color:#93c5fd;color:#1d4ed8}.runs-table{border:1px solid var(--line);border-radius:14px;padding:0;overflow:hidden;background:#fff;color:var(--muted)}.run-explorer.is-refreshing .runs-table{opacity:.65}.run-row{width:100%;border:0;border-bottom:1px solid var(--line);background:#fff;text-align:left;padding:12px;display:grid;grid-template-columns:12px minmax(0,1fr) auto;gap:8px;align-items:start;color:var(--ink);font:inherit;cursor:pointer}.run-row:last-child{border-bottom:0}.run-row:hover{background:#f8fafc}.run-row.selected{background:#eef4ff;box-shadow:inset 3px 0 0 var(--blue)}.run-row .run-id{font-size:13px;overflow-wrap:anywhere}.run-row span:nth-child(3),.run-row span:nth-child(4),.run-row span:nth-child(5),.run-row span:nth-child(6){grid-column:2;color:var(--muted);font-size:12px}.dot{width:9px;height:9px;border-radius:999px;background:#98a2b3;margin-top:5px}.dot.success{background:var(--green)}.dot.running{background:var(--blue)}.dot.warn{background:var(--amber)}.dot.error{background:var(--red)}.pill{font-style:normal;font-size:10px;border-radius:999px;padding:4px 7px;background:#f2f4f7;color:#475467;font-weight:850;white-space:nowrap}.run-detail-card{border:1px solid var(--line);border-radius:16px;background:#fff;padding:16px;min-width:0}.run-summary-card{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr) auto;gap:14px;align-items:start}.summary-meta{display:grid;grid-template-columns:1fr 1fr;gap:10px}.summary-meta div,.detail-grid>div,.diagnostic-list>div{border:1px solid var(--line);border-radius:11px;padding:9px;background:#fbfdff;min-width:0}.summary-meta span,.detail-grid span,.diagnostic-list span{display:block;color:var(--muted);font-size:11px;margin-bottom:4px}.summary-meta strong,.detail-grid strong,.diagnostic-list strong{font-size:13px;overflow-wrap:anywhere}.quick-links{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.progress-bar{height:9px;background:#e5e7eb;border-radius:999px;overflow:hidden;margin:16px 0 7px}.progress-bar div{height:100%;background:linear-gradient(90deg,var(--blue),var(--purple));border-radius:999px}.progress-percent{display:flex;justify-content:space-between;gap:14px;color:var(--muted);font-size:13px;margin-bottom:14px}.progress-percent strong{color:var(--blue)}.timeline{display:grid;grid-template-columns:repeat(8,minmax(86px,1fr));gap:8px;margin:12px 0 16px}.step{position:relative;display:grid;place-items:center;text-align:center;gap:6px;color:#475467;font-size:12px;min-height:70px}.step:not(:last-child):after{content:"";position:absolute;height:2px;background:#d0d5dd;left:63%;right:-42%;top:18px}.step.completed:not(:last-child):after,.step.done:not(:last-child):after{background:#22c55e}.step.running:not(:last-child):after{background:linear-gradient(90deg,#22c55e,#93c5fd)}.step-dot{width:30px;height:30px;border-radius:999px;display:grid;place-items:center;background:#fff;border:2px solid #d0d5dd;color:#667085;font-weight:900;z-index:1}.step.completed .step-dot,.step.done .step-dot{background:#22c55e;border-color:#22c55e;color:#fff}.step.running .step-dot{background:#fff;border-color:var(--blue);color:var(--blue);box-shadow:0 0 0 4px #dbeafe}.step.failed .step-dot,.step.blocked .step-dot{background:#fff;border-color:var(--red);color:var(--red)}.step small{display:block;color:var(--muted);font-size:10px;margin-top:2px}.manual-action{margin:12px 0;border:1px solid #f59e0b;background:var(--amber-soft);border-radius:14px;padding:13px;display:flex;justify-content:space-between;gap:12px;align-items:center}.manual-action p{margin:4px 0 0;color:#92400e;font-size:13px}.manual-action strong{color:#92400e}.poll-warning{margin:12px 0;border:1px solid #f59e0b;background:var(--amber-soft);color:#92400e;border-radius:12px;padding:10px 12px;font-size:13px}.detail-grid{display:grid;gap:10px}.detail-grid.three{grid-template-columns:1.2fr .9fr 1fr;margin-top:14px}.detail-grid.four{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:12px}.mini-panel{border:1px solid var(--line);border-radius:15px;background:#fbfdff;padding:13px;min-width:0}.activity-feed{display:grid;gap:8px}.activity-row{display:grid;grid-template-columns:62px 10px minmax(0,1fr) auto;gap:8px;align-items:start;padding:7px 0;border-bottom:1px solid var(--line)}.activity-row:last-child{border-bottom:0}.activity-row time{font-size:11px;color:var(--muted)}.activity-row .activity-dot{width:8px;height:8px;border-radius:999px;background:#98a2b3;margin-top:5px}.activity-row.success .activity-dot{background:var(--green)}.activity-row.running .activity-dot{background:var(--blue)}.activity-row.warning .activity-dot{background:var(--amber)}.activity-row.error .activity-dot{background:var(--red)}.activity-row strong{font-size:12px}.activity-row p{margin:2px 0 0;color:var(--muted);font-size:11px}.activity-row em{font-style:normal;font-size:10px;border:1px solid var(--line);border-radius:999px;padding:3px 6px;background:#fff;color:#667085;white-space:nowrap}.status-chips{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}.status-chip{border-radius:12px;padding:8px;background:#f2f4f7;display:flex;justify-content:space-between;gap:8px;font-size:11px}.status-chip em{font-style:normal}.issue-card{border-radius:12px;padding:10px;margin-top:10px;background:var(--amber-soft);border:1px solid #fde68a}.issue-card.error{background:var(--red-soft);border-color:#fecaca}.issue-card p{font-size:12px;color:#667085;margin:4px 0 0}.report-home p{font-size:12px;color:var(--muted);line-height:1.45}.empty{padding:13px;color:var(--muted);font-size:13px}.full-runs-grid{display:grid;grid-template-columns:360px minmax(0,1fr);gap:16px}.run-detail-card.expanded{min-height:540px}.extra-links{justify-content:flex-start;margin:10px 0}.split-panels{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:12px 0}.metric-list{list-style:none;margin:0;padding:0}.metric-list li{display:flex;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line);padding:9px 0;font-size:13px}.metric-list li:last-child{border-bottom:0}.metric-ok{color:var(--green)}.metric-warn{color:var(--amber)}.artifact-list{display:grid;gap:6px;font-size:13px}.artifact-list a{display:flex;justify-content:space-between;gap:8px;color:var(--blue);text-decoration:none;border-bottom:1px solid var(--line);padding:6px 0}.artifact-list em{font-style:normal;color:var(--muted)}.report-preview,.events{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;background:#101828;color:#d0d5dd;border:1px solid #1d2939;border-radius:12px;min-height:190px;max-height:380px;overflow:auto;padding:12px;white-space:pre-wrap;font-size:12px;line-height:1.5}.validate-grid{display:grid;grid-template-columns:330px minmax(0,1fr) 280px;gap:16px}.result-card{border:1px solid var(--line);border-radius:15px;background:#fbfdff;padding:14px}.about-panel{max-width:980px;margin:0 auto}.about-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.about-grid div{border:1px solid var(--line);border-radius:15px;background:#fbfdff;padding:14px}.blockers{border:1px solid #f59e0b;background:var(--amber-soft);border-radius:12px;padding:12px;margin:12px 0}.blockers strong{display:block;color:#92400e;font-size:13px}.blockers p{margin:4px 0 10px;color:#92400e;font-size:12px}.hidden-run-inspector{display:none!important}[hidden]{display:none!important}
12
+ @media(max-width:1320px){.topbar{grid-template-columns:1fr}.top-tabs{width:max-content}.top-actions{justify-content:flex-start}.build-workspace{grid-template-columns:1fr}.runs-layout{grid-template-columns:320px minmax(0,1fr)}.timeline{grid-template-columns:repeat(4,1fr)}.detail-grid.three,.validate-grid{grid-template-columns:1fr}.full-runs-grid{grid-template-columns:1fr}.run-summary-card{grid-template-columns:1fr}.quick-links{justify-content:flex-start}}
13
+ @media(max-width:820px){.app-shell{padding:12px}.brand p{display:none}.top-tabs{overflow:auto;width:100%}.nav-item{flex:1}.form-grid,.summary-meta,.detail-grid.four,.split-panels,.about-grid,.runs-layout{grid-template-columns:1fr}.timeline{grid-template-columns:1fr 1fr}.step:not(:last-child):after{display:none}.run-storage-card{grid-template-columns:1fr}.manual-action{display:block}.manual-action .inline-actions{margin-top:10px}.activity-row{grid-template-columns:56px 10px 1fr}.activity-row em{grid-column:3}}
 
 
 
 
 
 
 
 
 
web/static/app.js CHANGED
@@ -1,114 +1,43 @@
1
- const state={
2
- runId:null,
3
- bucketName:'space-factory-runs',
4
- bucketSource:null,
5
- bucketReady:false,
6
- poll:null,
7
- activeLinks:{},
8
- selectedRunDetail:null,
9
- runStatusFilter:'all',
10
- runsCache:[],
11
- runsLastLoaded:0,
12
- pollErrors:0,
13
- busy:false,
14
- user:null,
15
- };
16
- 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']);
17
- const DEFAULT_TIMELINE=[
18
- {label:'Bucket ready',status:'pending'},
19
- {label:'Job launched',status:'pending'},
20
- {label:'Dependencies',status:'pending'},
21
- {label:'Authenticated',status:'pending'},
22
- {label:'Model analysis',status:'pending'},
23
- {label:'Workspace',status:'pending'},
24
- {label:'Pi installed',status:'pending'},
25
- {label:'Pi running',status:'pending'},
26
- {label:'Create Space',status:'pending'},
27
- {label:'Upload files',status:'pending'},
28
- {label:'Hardware request',status:'pending'},
29
- {label:'API validation',status:'pending'},
30
- {label:'Smoke test',status:'pending'},
31
- {label:'Done',status:'pending'},
32
- ];
33
- function $(id){return document.getElementById(id)}
34
- function formValue(id){return ($(id)?.value||'').trim()}
35
- function setText(id,value){const el=$(id);if(el)el.textContent=value??'β€”'}
36
- function setInputValue(id,value){const el=$(id);if(!el)return;el.value=value||''}
37
- function escapeHtml(value){return String(value??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]))}
38
  function saveActiveRun(){try{localStorage.setItem('asf.activeRun',JSON.stringify({runId:state.runId,bucketName:state.bucketName,links:state.activeLinks||{}}))}catch(_){}}
39
  function clearSavedRun(){try{localStorage.removeItem('asf.activeRun')}catch(_){}}
40
- function restoreActiveRun(){try{const raw=localStorage.getItem('asf.activeRun');if(!raw)return;const saved=JSON.parse(raw);if(saved?.bucketName)state.bucketName=saved.bucketName;if(saved?.runId){state.runId=saved.runId;setText('detailRunId',saved.runId);setQuickLinks(saved.links||{});setBuildModeInspecting(saved.runId);startPolling(saved.runId)}}catch(_){}}
41
- let operationTimer=null;
42
- function setOperation(message,type='info'){
43
- const el=$('operationStatus');if(!el)return;
44
- if(operationTimer){clearTimeout(operationTimer);operationTimer=null}
45
- if(!message){el.hidden=true;el.textContent='';el.className='operation-status';return}
46
- el.hidden=false;el.className='operation-status '+type;el.textContent=message;
47
- if(type==='success'||type==='info')operationTimer=setTimeout(()=>{if(!state.busy)setOperation('')},7000)
48
- }
49
  function showMessage(msg,isError=false){const type=isError===true?'error':(typeof isError==='string'?isError:'info');setOperation(msg,type);const el=$('eventsPanel');if(el&&!state.runId)el.textContent=(isError?'Error: ':'')+msg;const warn=$('pollWarning');if(warn&&isError){warn.textContent=msg;warn.hidden=false}}
50
- function setBusy(isBusy,label='Working…'){
51
- state.busy=Boolean(isBusy);
52
- document.querySelectorAll('[data-action-button]').forEach(btn=>{btn.disabled=state.busy||btn.dataset.requiresBucket==='true'&&!state.bucketReady});
53
- if($('launchBuild'))$('launchBuild').disabled=state.busy||!state.bucketReady;
54
- if($('launchValidate'))$('launchValidate').disabled=state.busy;
55
- if(state.busy)setOperation(label,'loading')
56
- }
57
  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')}}
58
- function setBucketReady(ok,message){
59
- state.bucketReady=Boolean(ok);
60
- const btn=$('launchBuild');if(btn)btn.disabled=state.busy||!state.bucketReady;
61
- setText('buildGateText',state.bucketReady?'Bucket ready. You can launch builds.':(message||'Create your private run bucket before launching builds.'));
62
- setText('bucketStatusText',state.bucketReady?'Bucket ready':'Bucket not ready');
63
- setText('bucketInlineStatus',state.bucketReady?'Bucket ready':'Not ready');
64
- for(const id of ['bucketStatusDot','bucketInlineDot','sidebarBucketDot']){const dot=$(id);if(dot)dot.className='status-dot '+(state.bucketReady?'green':'amber-dot')}
65
- const inline=$('bucketInlineStatus');if(inline)inline.className='badge '+(state.bucketReady?'success':'warn');
66
- document.querySelectorAll('[data-requires-bucket="true"]').forEach(btn=>btn.disabled=state.busy||!state.bucketReady)
67
- }
68
  function effectivePreferredHardware(){const preferred=formValue('preferredHardware')||'zero-a10g';const fallback=formValue('fallbackHardware')||'l40sx1';const tryZero=$('tryZeroGpu')?.checked??true;if(tryZero)return preferred;if(preferred==='zero-a10g'||preferred.startsWith('zero'))return fallback;return preferred}
69
  function hardwarePlanLabel(){const tryZero=$('tryZeroGpu')?.checked??true;const preferred=effectivePreferredHardware();const fallback=formValue('fallbackHardware')||'l40sx1';const allow=$('allowFallback')?.checked??true;if(tryZero)return `ZeroGPU β†’ ${allow?fallback:'no fixed fallback'}`;return `${preferred}${allow&&fallback!==preferred?' β†’ '+fallback+' fallback':''}`}
70
  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''}
71
- function validateValidationForm(){if(!formValue('validateSpace'))return'Enter the target Space ID to validate.';try{JSON.parse(formValue('testArgs')||'[]');JSON.parse(formValue('testKwargs')||'{}')}catch(e){return`Invalid JSON payload: ${e.message}`;}return''}
72
- function artifactUrl(runId,bucketSource){if(!runId||!bucketSource)return'';return `https://huggingface.co/buckets/${bucketSource}/tree/main/runs/${runId}`}
73
- function normalizeLinks(links={}){const out={...links};const bucket=out.bucket_source||state.bucketSource;const run=out.run_id||state.runId;if(!out.artifacts_url&&bucket&&run)out.artifacts_url=artifactUrl(run,bucket);if(!out.job_url){const jobId=out.job_id||out.jobId;const owner=out.job_owner||out.owner||out.username||state.user?.username;if(jobId&&owner)out.job_url=`https://huggingface.co/jobs/${owner}/${jobId}`;}return out}
74
- function setQuickLinks(links={}){links=normalizeLinks(links||{});state.activeLinks=links;setLink('openJob',links.job_url);setLink('openSpace',links.target_space_url);setLink('openSettings',links.target_space_settings_url||links.target_space_url&&links.target_space_url+'/settings');setLink('openArtifacts',links.artifacts_url);setLink('manualOpenSettings',links.target_space_settings_url||links.target_space_url&&links.target_space_url+'/settings');renderManualAction({links})}
75
- function setBuildModeNew(){const panel=$('buildModePanel');if(panel){panel.classList.remove('inspecting');panel.innerHTML='<strong>Preparing a new build</strong><p>This form always starts a fresh private Space build. Selecting a run in Live progress only changes the progress and detail panels.</p>'}setText('progressTitle','Live job progress');setText('progressStatus','Idle')}
76
- function setBuildModeInspecting(runId){const panel=$('buildModePanel');if(panel){panel.classList.add('inspecting');panel.innerHTML=`<strong>Inspecting selected run</strong><p>You are viewing run <code>${escapeHtml(runId||'')}</code>. The build form is unchanged. Open New Build and click β€œPrepare new build” to clear the run context.</p>`}}
77
- function resetRunDetails(){
78
- for(const [id,value] of [['detailRunId','β€”'],['detailJobStatus','β€”'],['detailGateStatus','β€”'],['detailSpace','β€”'],['detailHardware','β€”'],['detailTraces','β€”'],['homeRunDetailStatus','No run']])setText(id,value);
79
- setQuickLinks({});
80
- const report=$('homeReportPreview');if(report)report.innerHTML='<strong>No run selected</strong><p>Select a run to preview its report and actions.</p>';
81
- const manual=$('manualActionPanel');if(manual)manual.hidden=true;
82
- }
83
- function prepareNewBuild(){
84
- if(state.poll){clearInterval(state.poll);state.poll=null}
85
- state.runId=null;state.selectedRunDetail=null;state.activeLinks={};state.pollErrors=0;clearSavedRun();
86
- document.querySelectorAll('.run-row').forEach(r=>r.classList.remove('selected'));
87
- setBuildModeNew();resetRunDetails();renderTimeline(DEFAULT_TIMELINE);setText('progressPercent','0%');const fill=$('progressFill');if(fill)fill.style.width='0%';setText('currentStep','Ready for a new build');setText('lastEvent','Choose a model card and launch a build.');setText('elapsed','β€”');setText('lastPolled','β€”');setText('hardwarePlan',hardwarePlanLabel());renderEvents([]);renderMetrics({});showMessage('New build mode ready. The form is independent from selected runs.','info')
88
- }
89
- function setActiveRun(result){state.runId=result.run_id;if(result.bucket_source){state.bucketSource=result.bucket_source}setText('detailRunId',result.run_id);setText('detailJobStatus',result.status||'RUNNING');setText('detailGateStatus',result.kind||'running');setText('detailSpace',result.target_space||'β€”');setText('detailHardware',result.selected_hardware||result.preferred_space_hardware||'β€”');setText('homeRunDetailStatus',result.status||'Running');setBuildModeInspecting(result.run_id);setQuickLinks({...result.links,bucket_source:result.bucket_source,run_id:result.run_id,job_id:result.job_id,job_owner:state.user?.username});saveActiveRun();switchView('progress');startPolling(result.run_id)}
90
  function startPolling(runId){state.runId=runId;state.pollErrors=0;const warn=$('pollWarning');if(warn)warn.hidden=true;if(state.poll)clearInterval(state.poll);saveActiveRun();refreshProgress();state.poll=setInterval(refreshProgress,4000)}
91
- async function loadAppInfo(){setOperation('Checking sign-in and run storage…','loading');try{const me=await apiGet('/api/me');state.user=me;setText('userChip','@'+me.username);const logout=$('logoutLink');if(logout){logout.hidden=false;logout.href=me.logout_url||'/logout'}setText('bucketLabel','Bucket: '+me.username+'/'+state.bucketName);setText('bucketInlineLabel',me.username+'/'+state.bucketName);renderAuthWarnings(me);await checkBucket(false);restoreActiveRun();if(!state.bucketReady)setOperation('Create your private run bucket before launching builds.','warning');else setOperation('');loadRuns(true)}catch(e){state.user=null;setText('userChip','Sign in');const logout=$('logoutLink');if(logout)logout.hidden=true;renderSignedOut();setBucketReady(false,'Sign in with Hugging Face first.');showMessage('Please sign in with Hugging Face, then refresh this page.',true)}}
 
 
92
  function renderSignedOut(){const auth=$('authPanel');if(!auth)return;auth.hidden=false;auth.className='auth-panel warning';auth.innerHTML='<strong>Sign in required</strong><p>Use Hugging Face OAuth before launching Jobs or reading your Bucket.</p><a class="link-btn" href="/oauth/huggingface/login">Sign in with Hugging Face β†—</a>'}
93
  function renderAuthWarnings(me){const auth=$('authPanel');if(!auth)return;const missing=me.missing_scopes||[];const warnings=me.warnings||[];if(!missing.length&&!warnings.length){auth.hidden=true;auth.innerHTML='';return}auth.hidden=false;auth.className='auth-panel warning';auth.innerHTML=`<strong>OAuth session warning</strong><p>${escapeHtml([...warnings,missing.length?'Missing scopes: '+missing.join(', '):''].filter(Boolean).join(' β€’ '))}</p><a class="link-btn" href="/oauth/huggingface/login">Refresh sign-in β†—</a>`}
94
- async function checkBucket(noisy=true){setButtonBusy('checkBucket',true,'Checking…');setButtonBusy('checkBucketSide',true,'Checking…');try{const data=await apiGet(`/api/bucket/status?bucket_name=${encodeURIComponent(state.bucketName)}`);const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketLabel','Bucket: '+source);setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Create your private run bucket before launching builds.');if(noisy)showMessage(data.ok?`Bucket ready: ${source}`:`Bucket not ready: ${data.error||'not found'}`,!data.ok);return data}catch(e){setBucketReady(false,e.message);if(noisy)showMessage(e.message,true);throw e}finally{setButtonBusy('checkBucket',false);setButtonBusy('checkBucketSide',false)}}
95
- async function createBucket(){setButtonBusy('createBucket',true,'Creating…');try{const data=await apiPost('/api/bucket/create',{bucket_name:state.bucketName});const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketLabel','Bucket: '+source);setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Could not create bucket.');showMessage(data.ok?`Created/ready: ${source}`:`Could not create bucket: ${data.error}`,!data.ok);if(data.ok)loadRuns(true)}catch(e){setBucketReady(false,e.message);showMessage(e.message,true)}finally{setButtonBusy('createBucket',false)}}
96
- function renderManualAction(p={}){const panel=$('manualActionPanel');if(!panel)return;const status=String(p.status||p.state?.status||p.inference_gate?.status||'');const manual=Boolean(p.manual_hardware_required||p.inference_gate?.manual_hardware_required||p.hardware_strategy?.manual_action_required||status.includes('manual_hardware_required')||status.includes('generated_needs_manual_hardware'));panel.hidden=!manual;if(!manual)return;setText('manualActionTitle','Manual hardware required');setText('manualActionText','Automatic hardware selection was not available. Open Space settings, choose the recommended GPU, then run Test Space.');setLink('manualOpenSettings',(p.links||state.activeLinks||{}).target_space_settings_url)}
97
- function prepareValidationFromActiveRun(){const detail=state.selectedRunDetail;const summary=detail?.summary||{};const target=summary.target_space||document.getElementById('detailSpace')?.textContent||'';if(target&&target!=='β€”')setInputValue('validateSpace',target);const expected=summary.expected_output_type||detail?.generation_smoke?.expected_output_type||'image';setInputValue('expectedOutput',expected);setInputValue('validateApi','/generate');switchView('validate');showMessage(`Prepared validation form for ${target||'selected Space'}.`,'info')}
98
- async function launchBuild(){const formError=validateBuildForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching build Job…');setButtonBusy('launchBuild',true,'Launching build…');try{setText('hardwarePlan',hardwarePlanLabel());const payload={bucket_name:state.bucketName,model_id:formValue('modelId'),target_space_name:formValue('targetSpace'),pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.checked??true};const result=await apiPost('/api/build',payload);setActiveRun(result);showMessage(`Build launched: ${result.run_id}`);loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchBuild',false)}}
99
- async function launchValidate(){const formError=validateValidationForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching validation Job…');setButtonBusy('launchValidate',true,'Launching validation…');try{const payload={bucket_name:state.bucketName,target_space_id:formValue('validateSpace'),api_name:formValue('validateApi')||'/generate',expected_output_type:formValue('expectedOutput')||'image',test_args_json:formValue('testArgs')||'[]',test_kwargs_json:formValue('testKwargs')||'{}',live_timeout_seconds:Number(formValue('validateTimeout')||1800)};const result=await apiPost('/api/validate',payload);setActiveRun(result);showMessage(`Validation launched: ${result.run_id}`);loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchValidate',false)}}
100
- async function refreshProgress(){if(!state.runId)return;try{const p=await apiGet(`/api/runs/${encodeURIComponent(state.runId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}`);state.pollErrors=0;const warn=$('pollWarning');if(warn)warn.hidden=true;renderProgress(p);renderEvents(p.events||[]);renderMetrics(p);renderManualAction(p);if(p.state){setText('detailJobStatus',p.state.status||p.status||'running');setText('detailGateStatus',p.inference_gate?.status||p.state.status||p.status||'β€”');setText('detailSpace',p.state.target_space||p.summary?.target_space||'β€”');setText('detailHardware',p.summary?.selected_hardware||p.state.selected_hardware||p.hardware_strategy?.selected_hardware||'β€”');setText('detailTraces',Array.isArray(p.events)?String(p.events.length)+' events':'β€”');setText('homeRunDetailStatus',p.state.status||p.status||'running');if(p.bucket_source)state.bucketSource=p.bucket_source;setQuickLinks({...p.links,bucket_source:p.bucket_source,run_id:p.run_id});saveActiveRun()}const status=String(p.status||p.state?.status||'').toLowerCase();if(TERMINAL_STATUSES.has(status)&&state.poll){clearInterval(state.poll);state.poll=null;setOperation(`Run finished with status: ${status}`,status.includes('success')?'success':status.includes('failed')||status.includes('blocker')?'error':'warning')}loadRuns(false)}catch(e){state.pollErrors+=1;console.warn(e);const warn=$('pollWarning');if(warn){warn.textContent=`Progress polling failed ${state.pollErrors} time${state.pollErrors===1?'':'s'}. The Job may still be running; retrying automatically…`;warn.hidden=false}}}
101
- function renderMetrics(p){const root=$('metricsList');if(!root)return;const smoke=p.generation_smoke||{};const gate=p.inference_gate||{};const health=(gate.implementation_signals&&gate.implementation_signals.health_passed)||smoke.health_passed;const smokeOk=smoke.ok||smoke.status==='success';const latency=smoke.latency_seconds;const duration=smoke.recommended_zerogpu_duration_seconds||smoke.recommended_duration_seconds;root.innerHTML=`<li><span>/health</span><strong class="${health?'metric-ok':''}">${health?'passed':'pending'}</strong></li><li><span>/generate smoke test</span><strong class="${smokeOk?'metric-ok':'metric-warn'}">${smokeOk?'passed':(smoke.status||'pending')}</strong></li><li><span>Latency</span><strong>${latency?fmtSeconds(Math.round(latency)):'β€”'}</strong></li><li><span>Recommended ZeroGPU duration</span><strong>${duration?fmtSeconds(Math.round(duration)):'β€”'}</strong></li>`}
102
  function renderEvents(events){const root=$('eventsPanel');if(!root)return;root.textContent=(events||[]).slice(-12).map(e=>`${e.ts||''} ${e.step||''} ${e.status||''} ${e.message||''}`).join('\n')||'No events yet. The run may still be scheduling.'}
103
- function switchView(view){document.querySelectorAll('.nav-item').forEach(b=>b.classList.toggle('active',b.dataset.view===view));document.querySelectorAll('[data-section]').forEach(s=>s.hidden=s.dataset.section!==view);setOperation('');const warn=$('pollWarning');if(warn)warn.hidden=true}
 
104
  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')))}
105
- function bindRunFilters(){document.querySelectorAll('.filter[data-status]').forEach(btn=>btn.addEventListener('click',()=>{state.runStatusFilter=btn.dataset.status||'all';document.querySelectorAll('.filter[data-status]').forEach(x=>x.classList.toggle('active',x.dataset.status===state.runStatusFilter));renderRunsFromCache()}))}
106
- function bindActions(){
107
- $('launchBuild')?.setAttribute('data-requires-bucket','true');$('launchBuild')?.setAttribute('data-action-button','true');$('launchValidate')?.setAttribute('data-action-button','true');
108
- $('launchBuild')?.addEventListener('click',launchBuild);$('launchValidate')?.addEventListener('click',launchValidate);$('prepareNewBuild')?.addEventListener('click',prepareNewBuild);
109
- $('manualGoValidate')?.addEventListener('click',prepareValidationFromActiveRun);$('homeValidateBtn')?.addEventListener('click',prepareValidationFromActiveRun);
110
- $('checkBucket')?.addEventListener('click',()=>checkBucket(true));$('createBucket')?.addEventListener('click',createBucket);$('checkBucketSide')?.addEventListener('click',()=>checkBucket(true));
111
- $('refreshRuns')?.addEventListener('click',()=>loadRuns(true));$('refreshRunsHome')?.addEventListener('click',()=>loadRuns(true));
112
- ['tryZeroGpu','preferredHardware','fallbackHardware','allowFallback'].forEach(id=>$(id)?.addEventListener('change',()=>setText('hardwarePlan',hardwarePlanLabel())))
113
- }
114
- bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());setBuildModeNew();loadAppInfo();renderTimeline(DEFAULT_TIMELINE);
 
1
+ const state={runId:null,bucketName:'space-factory-runs',bucketSource:null,bucketReady:false,poll:null,activeLinks:{},selectedRunDetail:null,runStatusFilter:'all',runSearch:'',runsCache:[],runsLastLoaded:0,pollErrors:0,busy:false,user:null,currentView:'build'};
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']);
3
+ const DEFAULT_TIMELINE=[{label:'Validate Input',status:'pending'},{label:'Analyze Model',status:'pending'},{label:'Generate App',status:'pending'},{label:'Create Space',status:'pending'},{label:'Hardware',status:'pending'},{label:'Live Validation',status:'pending'},{label:'Report',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(_){}}
6
+ function artifactUrl(runId,bucketSource){if(!runId||!bucketSource)return'';return `https://huggingface.co/buckets/${bucketSource}/tree/main/runs/${runId}`}
7
+ function normalizeLinks(links={}){const out={...links};const bucket=out.bucket_source||state.bucketSource;const run=out.run_id||state.runId;if(!out.artifacts_url&&bucket&&run)out.artifacts_url=artifactUrl(run,bucket);if(!out.job_url){const jobId=out.job_id||out.jobId;const owner=out.job_owner||out.owner||out.username||state.user?.username;if(jobId&&owner)out.job_url=`https://huggingface.co/jobs/${owner}/${jobId}`}return out}
8
+ function setQuickLinks(links={}){links=normalizeLinks(links||{});state.activeLinks=links;for(const id of ['openJob','selectedOpenJob'])setLink(id,links.job_url);for(const id of ['openSpace','selectedOpenSpace'])setLink(id,links.target_space_url);for(const id of ['openSettings','selectedOpenSettings','manualOpenSettings'])setLink(id,links.target_space_settings_url||links.target_space_url&&links.target_space_url+'/settings');for(const id of ['openArtifacts','selectedOpenArtifacts'])setLink(id,links.artifacts_url);renderManualAction({links})}
9
+ let operationTimer=null;function setOperation(message,type='info'){const el=$('operationStatus');if(!el)return;if(operationTimer){clearTimeout(operationTimer);operationTimer=null}if(!message){el.hidden=true;el.textContent='';el.className='operation-status';return}el.hidden=false;el.className='operation-status '+type;el.textContent=message;if(type==='success'||type==='info')operationTimer=setTimeout(()=>{if(!state.busy)setOperation('')},7000)}
 
 
 
 
 
10
  function showMessage(msg,isError=false){const type=isError===true?'error':(typeof isError==='string'?isError:'info');setOperation(msg,type);const el=$('eventsPanel');if(el&&!state.runId)el.textContent=(isError?'Error: ':'')+msg;const warn=$('pollWarning');if(warn&&isError){warn.textContent=msg;warn.hidden=false}}
11
+ function setBusy(isBusy,label='Working…'){state.busy=Boolean(isBusy);document.querySelectorAll('[data-action-button]').forEach(btn=>{btn.disabled=state.busy||btn.dataset.requiresBucket==='true'&&!state.bucketReady});if($('launchBuild'))$('launchBuild').disabled=state.busy||!state.bucketReady;if($('launchValidate'))$('launchValidate').disabled=state.busy;if(state.busy)setOperation(label,'loading')}
 
 
 
 
 
 
12
  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')}}
13
+ 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)}
 
 
 
 
 
 
 
 
 
14
  function effectivePreferredHardware(){const preferred=formValue('preferredHardware')||'zero-a10g';const fallback=formValue('fallbackHardware')||'l40sx1';const tryZero=$('tryZeroGpu')?.checked??true;if(tryZero)return preferred;if(preferred==='zero-a10g'||preferred.startsWith('zero'))return fallback;return preferred}
15
  function hardwarePlanLabel(){const tryZero=$('tryZeroGpu')?.checked??true;const preferred=effectivePreferredHardware();const fallback=formValue('fallbackHardware')||'l40sx1';const allow=$('allowFallback')?.checked??true;if(tryZero)return `ZeroGPU β†’ ${allow?fallback:'no fixed fallback'}`;return `${preferred}${allow&&fallback!==preferred?' β†’ '+fallback+' fallback':''}`}
16
  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''}
17
+ function validateValidationForm(){if(!formValue('validateSpace'))return'Enter the target Space ID to validate.';try{JSON.parse(formValue('testArgs')||'[]');JSON.parse(formValue('testKwargs')||'{}')}catch(e){return`Invalid JSON payload: ${e.message}`}return''}
18
+ function setBuildModeNew(){const panel=$('buildModePanel');if(panel){panel.classList.remove('inspecting');panel.innerHTML='<strong>Preparing a new build</strong><p>This form starts a fresh build. Selecting a run only updates the progress panels.</p>'}const status=$('progressStatus');if(status){status.textContent='Idle';status.className='badge neutral'}setText('progressTitle','No run selected')}
19
+ function setBuildModeInspecting(runId){const panel=$('buildModePanel');if(panel){panel.classList.add('inspecting');panel.innerHTML=`<strong>Inspecting selected run</strong><p>You are viewing run <code>${escapeHtml(runId||'')}</code>. The build form is unchanged.</p>`}}
20
+ function resetRunDetails(){for(const [id,value] of [['detailRunId','β€”'],['detailJobStatus','β€”'],['detailGateStatus','β€”'],['detailSpace','β€”'],['detailHardware','β€”'],['detailTraces','β€”'],['homeRunDetailStatus','No run'],['selectedRunId','β€”'],['selectedModelId','β€”'],['selectedTargetSpace','β€”'],['selectedHardware','β€”'],['runDetailStatus','β€”']])setText(id,value);setQuickLinks({});const report=$('homeReportPreview');if(report)report.innerHTML='<strong>No run selected</strong><p>Select a run to preview its report and actions.</p>';const full=$('reportPreview');if(full)full.textContent='No report selected.';const manual=$('manualActionPanel');if(manual)manual.hidden=true;const activity=$('activityFeed');if(activity)activity.innerHTML='<div class="empty">No activity yet.</div>'}
21
+ function prepareNewBuild(){if(state.poll){clearInterval(state.poll);state.poll=null}state.runId=null;state.selectedRunDetail=null;state.activeLinks={};state.pollErrors=0;clearSavedRun();document.querySelectorAll('.run-row').forEach(r=>r.classList.remove('selected'));setBuildModeNew();resetRunDetails();renderTimeline(DEFAULT_TIMELINE);setText('progressPercent','0%');const fill=$('progressFill');if(fill)fill.style.width='0%';setText('currentStep','Ready');setText('lastEvent','Choose a model card and launch a build.');setText('elapsed','β€”');setText('lastPolled','β€”');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')}
22
+ function setActiveRun(result){state.runId=result.run_id;if(result.bucket_source)state.bucketSource=result.bucket_source;setText('detailRunId',result.run_id);setText('detailJobStatus',result.status||'RUNNING');setText('detailGateStatus',result.kind||'running');setText('detailSpace',result.target_space||'β€”');setText('detailHardware',result.selected_hardware||result.preferred_space_hardware||'β€”');setText('homeRunDetailStatus',result.status||'Running');setBuildModeInspecting(result.run_id);setQuickLinks({...result.links,bucket_source:result.bucket_source,run_id:result.run_id,job_id:result.job_id,job_owner:state.user?.username});saveActiveRun();switchView('progress');startPolling(result.run_id)}
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  function startPolling(runId){state.runId=runId;state.pollErrors=0;const warn=$('pollWarning');if(warn)warn.hidden=true;if(state.poll)clearInterval(state.poll);saveActiveRun();refreshProgress();state.poll=setInterval(refreshProgress,4000)}
24
+ async function bootstrapRunRecovery(){return restoreActiveRun()}
25
+ async function restoreActiveRun(){try{const resumable=await apiGet(`/api/runs/resumable?bucket_name=${encodeURIComponent(state.bucketName)}`);if(resumable?.run?.run_id){state.runId=resumable.run.run_id;if(resumable.bucket_source)state.bucketSource=resumable.bucket_source;setBuildModeInspecting(state.runId);if(resumable.view)renderRunViewModel(resumable.view,{progress:0,events:[]});setQuickLinks(resumable.view?.links||{});startPolling(state.runId);showMessage(`Resumed latest active run: ${state.runId}`,'info');return true}}catch(e){console.warn('resumable lookup failed',e)}try{const raw=localStorage.getItem('asf.activeRun');if(!raw)return false;const saved=JSON.parse(raw);if(saved?.bucketName)state.bucketName=saved.bucketName;if(saved?.runId){state.runId=saved.runId;setText('detailRunId',saved.runId);setQuickLinks(saved.links||{});setBuildModeInspecting(saved.runId);startPolling(saved.runId);return true}}catch(_){}return false}
26
+ async function loadAppInfo(){setOperation('Checking sign-in and run storage…','loading');try{const me=await apiGet('/api/me');state.user=me;setText('userChip','@'+me.username);const logout=$('logoutLink');if(logout){logout.hidden=false;logout.href=me.logout_url||'/logout'}state.bucketSource=me.username+'/'+state.bucketName;setText('bucketInlineLabel',state.bucketSource);renderAuthWarnings(me);await checkBucket(false);await loadRuns(true);await bootstrapRunRecovery();if(!state.bucketReady)setOperation('Create your private run bucket before launching builds.','warning');else setOperation('')}catch(e){state.user=null;setText('userChip','Sign in');const logout=$('logoutLink');if(logout)logout.hidden=true;renderSignedOut();setBucketReady(false,'Sign in with Hugging Face first.');showMessage('Please sign in with Hugging Face, then refresh this page.',true)}}
27
  function renderSignedOut(){const auth=$('authPanel');if(!auth)return;auth.hidden=false;auth.className='auth-panel warning';auth.innerHTML='<strong>Sign in required</strong><p>Use Hugging Face OAuth before launching Jobs or reading your Bucket.</p><a class="link-btn" href="/oauth/huggingface/login">Sign in with Hugging Face β†—</a>'}
28
  function renderAuthWarnings(me){const auth=$('authPanel');if(!auth)return;const missing=me.missing_scopes||[];const warnings=me.warnings||[];if(!missing.length&&!warnings.length){auth.hidden=true;auth.innerHTML='';return}auth.hidden=false;auth.className='auth-panel warning';auth.innerHTML=`<strong>OAuth session warning</strong><p>${escapeHtml([...warnings,missing.length?'Missing scopes: '+missing.join(', '):''].filter(Boolean).join(' β€’ '))}</p><a class="link-btn" href="/oauth/huggingface/login">Refresh sign-in β†—</a>`}
29
+ async function checkBucket(noisy=true){setButtonBusy('checkBucket',true,'Checking…');try{const data=await apiGet(`/api/bucket/status?bucket_name=${encodeURIComponent(state.bucketName)}`);const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Create your private run bucket before launching builds.');if(noisy)showMessage(data.ok?`Bucket ready: ${source}`:`Bucket not ready: ${data.error||'not found'}`,!data.ok);return data}catch(e){setBucketReady(false,e.message);if(noisy)showMessage(e.message,true);throw e}finally{setButtonBusy('checkBucket',false)}}
30
+ async function createBucket(){setButtonBusy('createBucket',true,'Creating…');try{const data=await apiPost('/api/bucket/create',{bucket_name:state.bucketName});const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Could not create bucket.');showMessage(data.ok?`Created/ready: ${source}`:`Could not create bucket: ${data.error}`,!data.ok);if(data.ok)loadRuns(true)}catch(e){setBucketReady(false,e.message);showMessage(e.message,true)}finally{setButtonBusy('createBucket',false)}}
31
+ function renderManualAction(p={}){const panel=$('manualActionPanel');if(!panel)return;const view=p.view||{};const sm=view.status_model||{};const status=String(p.status||p.state?.status||p.inference_gate?.status||sm.global_status||'');const manual=Boolean(sm.requires_manual_action||p.manual_hardware_required||p.inference_gate?.manual_hardware_required||p.hardware_strategy?.manual_action_required||status.includes('manual_hardware_required')||status.includes('generated_needs_manual_hardware')||status.includes('waiting_manual_action'));panel.hidden=!manual;if(!manual)return;setText('manualActionTitle','Manual hardware required');setText('manualActionText','Automatic hardware selection was not available. Open Space settings, choose the recommended GPU, then run Space Test.');setLink('manualOpenSettings',(p.links||view.links||state.activeLinks||{}).target_space_settings_url)}
32
+ function prepareValidationFromActiveRun(){const detail=state.selectedRunDetail;const summary=detail?.summary||{};const target=summary.target_space||$('detailSpace')?.textContent||'';if(target&&target!=='β€”')setInputValue('validateSpace',target);const expected=summary.expected_output_type||detail?.generation_smoke?.expected_output_type||formValue('buildExpectedOutput')||'image';setInputValue('expectedOutput',expected);setInputValue('validateApi','/generate');switchView('validate');showMessage(`Prepared validation form for ${target||'selected Space'}.`,'info')}
33
+ async function launchBuild(){const formError=validateBuildForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching build Job…');setButtonBusy('launchBuild',true,'Launching…');try{setText('hardwarePlan',hardwarePlanLabel());const payload={bucket_name:state.bucketName,run_id:formValue('runId')||undefined,model_id:formValue('modelId'),target_space_name:formValue('targetSpace'),pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.checked??true,expected_output_type:formValue('buildExpectedOutput')};const result=await apiPost('/api/build',payload);setActiveRun(result);showMessage(`Build launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchBuild',false)}}
34
+ async function launchValidate(){const formError=validateValidationForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching validation Job…');setButtonBusy('launchValidate',true,'Launching…');try{const payload={bucket_name:state.bucketName,target_space_id:formValue('validateSpace'),api_name:formValue('validateApi')||'/generate',expected_output_type:formValue('expectedOutput')||'image',test_args_json:formValue('testArgs')||'[]',test_kwargs_json:formValue('testKwargs')||'{}',live_timeout_seconds:Number(formValue('validateTimeout')||1800)};const result=await apiPost('/api/validate',payload);setActiveRun(result);showMessage(`Validation launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchValidate',false)}}
35
+ async function refreshProgress(){if(!state.runId)return;try{const p=await apiGet(`/api/runs/${encodeURIComponent(state.runId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}`);state.pollErrors=0;const warn=$('pollWarning');if(warn)warn.hidden=true;renderProgress(p);renderEvents(p.events||[]);renderMetrics(p);if(p.state||p.summary){if(p.bucket_source)state.bucketSource=p.bucket_source;setText('detailJobStatus',p.state?.status||p.status||'running');setText('detailGateStatus',p.inference_gate?.status||p.state?.status||p.status||'β€”');setText('detailSpace',p.state?.target_space||p.summary?.target_space||'β€”');setText('detailHardware',p.summary?.selected_hardware||p.state?.selected_hardware||p.hardware_strategy?.selected_hardware||'β€”');setText('detailTraces',Array.isArray(p.events)?String(p.events.length)+' events':'β€”');setText('homeRunDetailStatus',p.view?.header?.status_label||p.state?.status||p.status||'running');setQuickLinks({...p.links,...p.view?.links,bucket_source:p.bucket_source,run_id:p.run_id});saveActiveRun()}const status=String(p.view?.header?.status||p.status||p.state?.status||'').toLowerCase();if(TERMINAL_STATUSES.has(status)&&state.poll){clearInterval(state.poll);state.poll=null;setOperation(`Run finished with status: ${badgeLabel(status)}`,status.includes('success')||status.includes('succeed')?'success':status.includes('failed')||status.includes('blocker')?'error':'warning')}loadRuns(false)}catch(e){state.pollErrors+=1;console.warn(e);const warn=$('pollWarning');if(warn){warn.textContent=`Progress polling failed ${state.pollErrors} time${state.pollErrors===1?'':'s'}. The Job may still be running; retrying automatically…`;warn.hidden=false}}}
36
+ 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>`}
37
  function renderEvents(events){const root=$('eventsPanel');if(!root)return;root.textContent=(events||[]).slice(-12).map(e=>`${e.ts||''} ${e.step||''} ${e.status||''} ${e.message||''}`).join('\n')||'No events yet. The run may still be scheduling.'}
38
+ async function resumeLatestRun(){try{const data=await apiGet(`/api/runs/resumable?bucket_name=${encodeURIComponent(state.bucketName)}`);if(!data.run){showMessage('No resumable run found in this bucket.','warning');return}state.runId=data.run.run_id;if(data.bucket_source)state.bucketSource=data.bucket_source;if(data.view)renderRunViewModel(data.view,{progress:0,events:[]});setBuildModeInspecting(state.runId);setQuickLinks(data.view?.links||{});startPolling(state.runId);showMessage(`Resumed ${state.runId}`,'success')}catch(e){showMessage(e.message,true)}}
39
+ function switchView(view){state.currentView=view;document.querySelectorAll('.nav-item').forEach(b=>b.classList.toggle('active',b.dataset.view===view));document.querySelectorAll('[data-section]').forEach(s=>s.hidden=s.dataset.section!==view);setOperation('');const warn=$('pollWarning');if(warn)warn.hidden=true;if(view==='progress')loadRuns(false)}
40
  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')))}
41
+ function bindRunFilters(){document.querySelectorAll('.filter[data-status]').forEach(btn=>btn.addEventListener('click',()=>{state.runStatusFilter=btn.dataset.status||'all';renderRunsFromCache()}));for(const id of ['runSearch','runSearchFull'])$(id)?.addEventListener('input',e=>{state.runSearch=e.target.value||'';for(const other of ['runSearch','runSearchFull'])if($(other)&&$(other)!==e.target)$(other).value=state.runSearch;clearTimeout(state.runSearchTimer);state.runSearchTimer=setTimeout(renderRunsFromCache,80)})}
42
+ function bindActions(){$('launchBuild')?.setAttribute('data-requires-bucket','true');$('launchBuild')?.setAttribute('data-action-button','true');$('launchValidate')?.setAttribute('data-action-button','true');$('launchBuild')?.addEventListener('click',launchBuild);$('launchValidate')?.addEventListener('click',launchValidate);$('prepareNewBuild')?.addEventListener('click',prepareNewBuild);$('manualGoValidate')?.addEventListener('click',prepareValidationFromActiveRun);$('homeValidateBtn')?.addEventListener('click',prepareValidationFromActiveRun);$('selectedValidateBtn')?.addEventListener('click',prepareValidationFromActiveRun);$('checkBucket')?.addEventListener('click',()=>checkBucket(true));$('bucketStatusPill')?.addEventListener('click',()=>checkBucket(true));$('createBucket')?.addEventListener('click',createBucket);$('refreshRuns')?.addEventListener('click',()=>loadRuns(true));$('refreshRunsHome')?.addEventListener('click',()=>loadRuns(true));$('resumeLatestRun')?.addEventListener('click',resumeLatestRun);$('resumeLatestRunFull')?.addEventListener('click',resumeLatestRun);['tryZeroGpu','preferredHardware','fallbackHardware','allowFallback'].forEach(id=>$(id)?.addEventListener('change',()=>setText('hardwarePlan',hardwarePlanLabel())))}
43
+ bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());setBuildModeNew();renderTimeline(DEFAULT_TIMELINE);loadAppInfo();
 
 
 
 
 
 
 
web/static/components.js CHANGED
@@ -1 +1,11 @@
1
- function fmtSeconds(s){if(s==null)return"β€”";s=Number(s)||0;const m=Math.floor(s/60),r=s%60;return m?`${m}m ${r}s`:`${r}s`}function renderTimeline(items){const root=document.getElementById("timeline");if(!root)return;root.innerHTML=(items||[]).map(x=>`<div class="step ${x.status}"><div class="step-dot">${x.status==="done"?"βœ“":x.status==="failed"?"!":""}</div><div>${x.label}</div></div>`).join("")}function setLink(id,url){const el=document.getElementById(id);if(!el)return;if(url){el.href=url;el.classList.remove("disabled")}else{el.removeAttribute("href");el.classList.add("disabled")}}
 
 
 
 
 
 
 
 
 
 
 
1
+ function $(id){return document.getElementById(id)}
2
+ 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)||'β€”'}}
9
+ function statusClass(status){status=String(status||'').toLowerCase();if(status.includes('succeed')||status.includes('success')||status.includes('passed')||status==='completed')return'success';if(status.includes('manual')||status.includes('stale')||status.includes('blocked'))return'warn';if(status.includes('failed')||status.includes('error')||status.includes('technical'))return'error';if(status.includes('running')||status.includes('queued')||status.includes('pending')||status.includes('building'))return'running';return'neutral'}
10
+ function setLink(id,url){const el=$(id);if(!el)return;if(url){el.href=url;el.classList.remove('disabled');el.setAttribute('aria-disabled','false')}else{el.removeAttribute('href');el.classList.add('disabled');el.setAttribute('aria-disabled','true')}}
11
+ function badgeLabel(status){return String(status||'unknown').replaceAll('_',' ')}
web/static/progress.js CHANGED
@@ -1,18 +1,8 @@
1
- function renderProgress(p){
2
- const progress=Number(p.progress||0);
3
- setText('progressPercent',`${progress}%`);
4
- const fill=document.getElementById('progressFill');
5
- if(fill)fill.style.width=`${Math.max(0,Math.min(100,progress))}%`;
6
- setText('currentStep',p.current_step_label||'β€”');
7
- setText('lastEvent',p.last_event||'β€”');
8
- setText('elapsed',fmtSeconds(p.elapsed_seconds));
9
- setText('progressTitle',p.current_step_label||'Live job progress');
10
- setText('progressStatus',p.status||'running');
11
- setText('lastPolled',new Date().toLocaleTimeString());
12
- renderTimeline(p.timeline||[]);
13
- }
14
- function fmtSeconds(s){if(s==null)return"β€”";s=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`}
15
- function timelineIcon(status){if(status==='done')return'βœ“';if(status==='running')return'…';if(status==='failed')return'!';return'β—‹'}
16
- function timelineStatus(status){if(status==='done')return'completed';if(status==='running')return'active';if(status==='failed')return'failed';return'pending'}
17
- function renderTimeline(items){const root=document.getElementById('timeline');if(!root)return;root.innerHTML=(items||[]).map((x,i)=>{const status=x.status||'pending';return `<div class="step ${status}" data-status="${timelineStatus(status)}"><div class="step-dot">${timelineIcon(status)}</div><div>${escapeHtml(x.label||`Step ${i+1}`)}</div></div>`}).join('')}
18
- function setLink(id,url){const el=document.getElementById(id);if(!el)return;if(url){el.href=url;el.classList.remove('disabled');el.setAttribute('aria-disabled','false')}else{el.removeAttribute('href');el.classList.add('disabled');el.setAttribute('aria-disabled','true')}}
 
1
+ function timelineIcon(status){status=String(status||'pending');if(status==='done'||status==='completed')return'βœ“';if(status==='running')return'β€’';if(status==='failed'||status==='blocked')return'!';if(status==='skipped')return'β†·';return'β—‹'}
2
+ function timelineStatus(status){status=String(status||'pending');if(status==='done'||status==='completed')return'completed';if(status==='running')return'running';if(status==='failed')return'failed';if(status==='blocked')return'blocked';return'pending'}
3
+ function renderTimeline(items){const root=$('timeline');if(!root)return;root.innerHTML=(items||[]).map((x,i)=>{const st=timelineStatus(x.status);return `<div class="step ${st}" data-status="${st}"><div class="step-dot">${timelineIcon(x.status)}</div><div><strong>${escapeHtml(x.label||`Step ${i+1}`)}</strong><small>${st}</small></div></div>`}).join('')}
4
+ function renderActivityFeed(items){const root=$('activityFeed');if(!root)return;if(!items.length){root.innerHTML='<div class="empty">No activity yet. Waiting for the first worker event.</div>';return}root.innerHTML=items.slice(-8).reverse().map(item=>`<div class="activity-row ${statusClass(item.severity||item.status)}"><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(item.status||'')}</p></div><em>${escapeHtml(item.agent||'Factory Agent')}</em></div>`).join('')}
5
+ function renderDiagnostics(d={},header={}){setText('diagBuildStatus',badgeLabel(d.build_status||'pending'));setText('diagBuildStep',header.current_phase_label||'Waiting for run data');const chips=$('statusChips');if(chips){const data=[['Build',d.build_status],['API',d.api_status],['Tests',d.tests_status],['Verdict',d.verdict_status]];chips.innerHTML=data.map(([k,v])=>`<span class="status-chip ${statusClass(v)}"><strong>${escapeHtml(k)}</strong><em>${escapeHtml(badgeLabel(v||'pending'))}</em></span>`).join('')}const issue=$('issueCard');if(issue){const has=Boolean(d.issue&&d.issue.title);issue.hidden=!has;if(has){issue.className='issue-card '+statusClass(d.issue.status||d.issue.title);setText('issueTitle',d.issue.title);setText('issueDetail',d.issue.detail||d.issue.status||'Review logs.')}}const rules=$('zeroGpuChecklist');if(rules)rules.innerHTML=(d.zerogpu_rules||[]).map(r=>`<li class="${statusClass(r.status)}"><span>${escapeHtml(r.label)}</span><strong>${escapeHtml(r.status||'pending')}</strong></li>`).join('')}
6
+ function renderSpaceTestPreview(s={}){const st=$('spaceTestStatus');if(st){st.textContent=badgeLabel(s.status||'Pending');st.className='badge '+statusClass(s.status)}setText('spaceTestVerdict',badgeLabel(s.verdict||'pending'));setText('spaceTestOutput',s.output_artifact?'Output artifact ready':'Waiting for API test')}
7
+ function renderRunViewModel(view,p={}){const header=view.header||{};const sm=view.status_model||{};const progress=Number(sm.global_status==='succeeded'?100:(p.progress||0));const fill=$('progressFill');if(fill)fill.style.width=`${Math.max(0,Math.min(100,progress))}%`;setText('progressPercent',`${Math.round(progress)}%`);setText('progressTitle',header.title||view.run_id||'Live job progress');const statusEl=$('progressStatus');if(statusEl){statusEl.textContent=header.status_label||badgeLabel(header.status||'Running');statusEl.className='badge '+statusClass(header.status)}setText('currentStep',header.current_phase_label||'β€”');setText('lastEvent',(view.activity||[]).slice(-1)[0]?.message||p.last_event||'Waiting for worker events.');setText('elapsed',fmtSeconds(header.elapsed_seconds||p.elapsed_seconds));setText('lastPolled',new Date().toLocaleTimeString());setText('detailSpace',header.space||'β€”');setText('detailJobStatus',header.raw_status||header.status||'β€”');setText('detailGateStatus',sm.verdict||'β€”');setText('detailTraces',Array.isArray(p.events)?`${p.events.length} events`:'β€”');setLink('openJob',view.links?.job_url);setLink('openSpace',view.links?.target_space_url);setLink('openSettings',view.links?.target_space_settings_url);setLink('openArtifacts',view.links?.artifacts_url);renderTimeline(view.pipeline||[]);renderActivityFeed(view.activity||[]);renderDiagnostics(view.diagnostics||{},header);renderSpaceTestPreview(view.space_test||{});renderManualAction({view,...p})}
8
+ function renderProgress(p){if(p.view){renderRunViewModel(p.view,p);return}const progress=Number(p.progress||0);const fill=$('progressFill');if(fill)fill.style.width=`${Math.max(0,Math.min(100,progress))}%`;setText('progressPercent',`${progress}%`);setText('currentStep',p.current_step_label||'β€”');setText('lastEvent',p.last_event||'β€”');setText('elapsed',fmtSeconds(p.elapsed_seconds));setText('progressTitle',p.current_step_label||'Live job progress');const statusEl=$('progressStatus');if(statusEl){statusEl.textContent=p.status||'running';statusEl.className='badge '+statusClass(p.status)}setText('lastPolled',new Date().toLocaleTimeString());renderTimeline(p.timeline||[])}
 
 
 
 
 
 
 
 
 
 
web/static/runs.js CHANGED
@@ -1,134 +1,12 @@
1
- function statusClass(status){status=String(status||'unknown');if(status.includes('success'))return'success';if(status.includes('manual'))return'warn';if(status.includes('blocker')||status.includes('failed'))return'error';if(status.includes('running')||status==='RUNNING')return'running';return''}
2
- function shortText(value,max=42){value=String(value||'β€”');return value.length>max?value.slice(0,max-1)+'…':value}
3
-
4
- function inferredJobUrl(summary={}, detail={}){
5
- if(summary.job_url)return summary.job_url;
6
- const launch=detail.launch||{};
7
- if(launch.job_url)return launch.job_url;
8
- const jobId=summary.job_id||launch.job_id||detail.state?.job_id;
9
- const owner=launch.created_by||detail.state?.created_by||state.user?.username;
10
- return jobId&&owner?`https://huggingface.co/jobs/${owner}/${jobId}`:'';
11
- }
12
-
13
- function runMatchesStatus(run,status){
14
- const s=String(run.status||'unknown').toLowerCase();
15
- if(!status||status==='all')return true;
16
- if(status==='running')return s.includes('running')||s==='queued'||s==='pending';
17
- if(status==='full_inference_success')return s.includes('success');
18
- if(status==='manual_hardware_required')return s.includes('manual')||s.includes('hardware');
19
- if(status==='technical_blocker')return s.includes('blocker');
20
- if(status==='failed')return s.includes('failed')||s.includes('error');
21
- return s===String(status).toLowerCase();
22
- }
23
- function runMatchesQuery(run,query){
24
- if(!query)return true;
25
- const haystack=[run.run_id,run.model_id,run.target_space,run.status,run.kind,run.selected_hardware].map(x=>String(x||'').toLowerCase()).join(' ');
26
- return haystack.includes(String(query).toLowerCase());
27
- }
28
- function filteredRuns(){
29
- const query=document.getElementById('runSearch')?.value||'';
30
- const status=state.runStatusFilter||'all';
31
- return (state.runsCache||[]).filter(r=>runMatchesStatus(r,status)&&runMatchesQuery(r,query));
32
- }
33
- function renderRunStats(){
34
- const counts={all:state.runsCache.length,running:0,full_inference_success:0,manual_hardware_required:0,failed:0};
35
- for(const r of state.runsCache){
36
- if(runMatchesStatus(r,'running'))counts.running+=1;
37
- if(runMatchesStatus(r,'full_inference_success'))counts.full_inference_success+=1;
38
- if(runMatchesStatus(r,'manual_hardware_required'))counts.manual_hardware_required+=1;
39
- if(runMatchesStatus(r,'failed'))counts.failed+=1;
40
- }
41
- document.querySelectorAll('.filter[data-status]').forEach(btn=>{
42
- const status=btn.dataset.status||'all';
43
- const label={all:'All',running:'Running',full_inference_success:'Success',manual_hardware_required:'Manual',failed:'Failed',technical_blocker:'Blockers'}[status]||status;
44
- const count=counts[status];
45
- btn.textContent=typeof count==='number'?`${label} ${count}`:label;
46
- });
47
- }
48
- function renderSelectedLinks(summary={}){
49
- const detail=state.selectedRunDetail||{};
50
- const runId=summary.run_id||detail.run_id;
51
- const bucket=summary.bucket_source||detail.bucket_source||state.bucketSource;
52
- const artifacts=summary.artifacts_url||(typeof artifactUrl==='function'?artifactUrl(runId,bucket):'');
53
- const jobUrl=inferredJobUrl(summary,detail);
54
- setLink('selectedOpenJob',jobUrl);
55
- setLink('selectedOpenSpace',summary.target_space_url);
56
- setLink('selectedOpenSettings',summary.target_space_url?summary.target_space_url+'/settings':'');
57
- setLink('selectedOpenArtifacts',artifacts);
58
- const btn=document.getElementById('selectedValidateBtn');
59
- if(btn){btn.disabled=!summary.target_space;btn.onclick=()=>{state.selectedRunDetail=state.selectedRunDetail||{summary};prepareValidationFromActiveRun();}}
60
- }
61
- function renderBlockers(detail={}){
62
- const root=document.getElementById('selectedBlockers');if(!root)return;
63
- const summary=detail.summary||{};const blockers=detail.technical_blockers||{};
64
- const manual=summary.manual_hardware_required||String(summary.status||'').includes('manual_hardware_required');
65
- const blockerItems=Array.isArray(blockers.blockers)?blockers.blockers:[];
66
- if(!manual&&!blockerItems.length){root.hidden=true;root.innerHTML='';return;}
67
- root.hidden=false;const parts=[];
68
- if(manual)parts.push('<strong>Manual hardware required</strong><p>Open Space settings, choose the recommended GPU, then run validation.</p>');
69
- for(const b of blockerItems.slice(0,4)){parts.push(`<strong>${shortText(b.type||b.name||'Technical blocker',32)}</strong><p>${shortText(b.claim||b.reason||b.message||'',120)}</p>`)}
70
- root.innerHTML=parts.join('');
71
- }
72
- function renderRunRows(runs,compact=false){
73
- return (runs||[]).map(r=>`<button class="run-row" data-run="${escapeHtml(r.run_id)}"><span class="dot ${statusClass(r.status)}"></span><strong class="run-id" title="${escapeHtml(r.run_id)}">${shortText(r.run_id,compact?22:26)}</strong><span title="${escapeHtml(r.model_id||'')}">${shortText(r.model_id,compact?22:24)}</span><em class="pill run-status ${statusClass(r.status)}">${r.status||'unknown'}</em><span title="${escapeHtml(r.target_space||'')}">${shortText(r.target_space,compact?24:28)}</span><span>${r.selected_hardware||'β€”'}</span></button>`).join('')
74
- }
75
- function renderRunsFromCache(){
76
- const runs=filteredRuns();
77
- const roots=[['runsTable',false],['runsTableHome',true]];
78
- for(const [id,compact] of roots){
79
- const root=document.getElementById(id);
80
- if(root)root.innerHTML=renderRunRows(runs,compact)||'<div class="empty">No runs match this filter.</div>';
81
- }
82
- renderRunStats();
83
- document.querySelectorAll('.run-row[data-run]').forEach(row=>row.addEventListener('click',()=>selectRun(row.dataset.run)));
84
- if(state.runId)document.querySelectorAll('.run-row').forEach(row=>row.classList.toggle('selected',row.dataset.run===state.runId));
85
- }
86
- async function loadRuns(forceNetwork=false){
87
- const stale=Date.now()-(state.runsLastLoaded||0)>45000;
88
- const shouldFetch=forceNetwork||!state.runsCache.length||stale;
89
- try{
90
- if(shouldFetch){
91
- document.querySelectorAll('.run-explorer').forEach(x=>x.classList.add('is-refreshing'));
92
- const data=await apiGet(`/api/runs?limit=100&bucket_name=${encodeURIComponent(state.bucketName)}`);
93
- state.runsCache=data.runs||[];
94
- state.runsLastLoaded=Date.now();
95
- if(data.bucket_source)state.bucketSource=data.bucket_source;
96
- }
97
- renderRunsFromCache();
98
- }catch(e){
99
- for(const id of ['runsTable','runsTableHome']){const root=document.getElementById(id);if(root)root.textContent='Could not load runs: '+e.message}
100
- if(forceNetwork)console.warn(e);
101
- }finally{
102
- document.querySelectorAll('.run-explorer').forEach(x=>x.classList.remove('is-refreshing'));
103
- }
104
- }
105
- async function selectRun(runId){
106
- if(!runId)return;
107
- document.querySelectorAll('.run-row').forEach(row=>row.classList.toggle('selected',row.dataset.run===runId));
108
- setBuildModeInspecting(runId);
109
- try{
110
- const detail=await apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}`);
111
- if(detail.bucket_source)state.bucketSource=detail.bucket_source;
112
- renderRunDetail(detail);
113
- startPolling(runId);
114
- }catch(e){
115
- const root=document.getElementById('reportPreview');if(root)root.textContent='Could not load run details: '+e.message;
116
- const home=document.getElementById('homeReportPreview');if(home)home.innerHTML=`<strong>Could not load run details</strong><p>${escapeHtml(e.message)}</p>`;
117
- }
118
- }
119
- function renderRunDetail(detail){
120
- state.selectedRunDetail=detail;
121
- const summary=detail.summary||{};const smoke=detail.generation_smoke||{};const files=detail.files||[];
122
- setText('selectedRunId',detail.run_id||summary.run_id||'β€”');setText('selectedModelId',summary.model_id||'β€”');setText('selectedTargetSpace',summary.target_space||'β€”');setText('selectedHardware',summary.selected_hardware||'β€”');
123
- setText('runDetailStatus',summary.status||'unknown');setText('homeRunDetailStatus',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||'β€”');
124
- const statusEl=document.getElementById('runDetailStatus');if(statusEl)statusEl.className='badge '+statusClass(summary.status);const homeStatus=document.getElementById('homeRunDetailStatus');if(homeStatus)homeStatus.className='badge '+statusClass(summary.status);
125
- const validation=document.getElementById('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>`}
126
- const artifactRoot=document.getElementById('artifactList');if(artifactRoot){const interesting=files.filter(f=>!/traces\//.test(f.path||'')).slice(0,18);artifactRoot.innerHTML=interesting.length?interesting.map(f=>`<a href="${f.url}" target="_blank"><span>${f.path}</span><em>${f.size?Math.round(Number(f.size)/1024)+' KB':''}</em></a>`).join(''):'No artifacts indexed yet.'}
127
- const reportText=(detail.report||'No report available.').slice(0,5000);const report=document.getElementById('reportPreview');if(report)report.textContent=reportText;
128
- const homeReport=document.getElementById('homeReportPreview');if(homeReport){const firstLine=reportText.split('\n').find(Boolean)||'No report available yet.';homeReport.innerHTML=`<span class="status-dot ${statusClass(summary.status)==='error'?'amber-dot':'green'}"></span><strong>${escapeHtml((detail.inference_gate||{}).status||summary.status||'Run selected')}</strong><p>${escapeHtml(shortText(firstLine,180))}</p>`}
129
- 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});
130
- renderSelectedLinks({...summary,bucket_source:detail.bucket_source});renderBlockers(detail);
131
- const homeBtn=document.getElementById('homeValidateBtn');if(homeBtn){homeBtn.disabled=!summary.target_space;homeBtn.onclick=prepareValidationFromActiveRun}
132
- }
133
- let runSearchTimer=null;
134
- document.getElementById('runSearch')?.addEventListener('input',()=>{clearTimeout(runSearchTimer);runSearchTimer=setTimeout(renderRunsFromCache,90)});
 
1
+ function inferredJobUrl(summary={}, detail={}){if(summary.job_url)return summary.job_url;const launch=detail.launch||{};if(launch.job_url)return launch.job_url;const jobId=summary.job_id||launch.job_id||detail.state?.job_id;const owner=launch.created_by||detail.state?.created_by||state.user?.username;return jobId&&owner?`https://huggingface.co/jobs/${owner}/${jobId}`:''}
2
+ function runMatchesStatus(run,status){const s=String(run.status||'unknown').toLowerCase();if(!status||status==='all')return true;if(status==='running')return s.includes('running')||s==='queued'||s==='pending'||s==='started'||s==='waiting';if(status==='full_inference_success')return s.includes('success')||s.includes('passed');if(status==='manual_hardware_required')return s.includes('manual')||s.includes('hardware');if(status==='technical_blocker')return s.includes('blocker');if(status==='failed')return s.includes('failed')||s.includes('error');return s===String(status).toLowerCase()}
3
+ function runMatchesQuery(run,query){if(!query)return true;const haystack=[run.run_id,run.model_id,run.target_space,run.status,run.kind,run.selected_hardware].map(x=>String(x||'').toLowerCase()).join(' ');return haystack.includes(String(query).toLowerCase())}
4
+ function filteredRuns(){return (state.runsCache||[]).filter(r=>runMatchesStatus(r,state.runStatusFilter||'all')&&runMatchesQuery(r,state.runSearch||''))}
5
+ function renderRunStats(){const counts={all:state.runsCache.length,running:0,full_inference_success:0,manual_hardware_required:0,failed: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}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'}[status]||status;const count=counts[status];btn.textContent=typeof count==='number'?`${label} ${count}`:label;btn.classList.toggle('active',status===state.runStatusFilter)})}
6
+ function renderRunRows(runs,compact=false){return (runs||[]).map(r=>`<button class="run-row" data-run="${escapeHtml(r.run_id)}"><span class="dot ${statusClass(r.status)}"></span><strong class="run-id" title="${escapeHtml(r.run_id)}">${shortText(r.run_id,compact?24:30)}</strong><em class="pill ${statusClass(r.status)}">${escapeHtml(badgeLabel(r.status||'unknown'))}</em><span title="${escapeHtml(r.model_id||'')}">${shortText(r.model_id,compact?28:34)}</span><span title="${escapeHtml(r.target_space||'')}">${shortText(r.target_space,compact?28:34)}</span><span>${escapeHtml(r.selected_hardware||'β€”')}</span></button>`).join('')}
7
+ function renderRunsFromCache(){const runs=filteredRuns();for(const [id,compact] of [['runsTableHome',true],['runsTable',false]]){const root=$(id);if(root)root.innerHTML=renderRunRows(runs,compact)||'<div class="empty">No runs match this filter.</div>'}renderRunStats();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))}
8
+ 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=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'))}}
9
+ 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);switchView(state.currentView==='runs'?'runs':'build')}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>`}}
10
+ 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);setLink('selectedOpenSpace',summary.target_space_url);setLink('selectedOpenSettings',summary.target_space_url?summary.target_space_url+'/settings':'');setLink('selectedOpenArtifacts',artifacts);const btn=$('selectedValidateBtn');if(btn){btn.disabled=!summary.target_space;btn.onclick=prepareValidationFromActiveRun}}
11
+ 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('')}
12
+ 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('selectedTargetSpace',summary.target_space||'β€”');setText('selectedHardware',summary.selected_hardware||summary.hardware||'β€”');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(firstLine,180))}</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});if(view.schema_version)renderRunViewModel(view,{...detail,progress:detail.progress||0,events:detail.events||[]});const homeBtn=$('homeValidateBtn');if(homeBtn){homeBtn.disabled=!summary.target_space;homeBtn.onclick=prepareValidationFromActiveRun}}