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