fffiloni commited on
Commit
91ffb7f
·
verified ·
1 Parent(s): edde694

Upload 6 files

Browse files
Files changed (4) hide show
  1. web/static/app.css +303 -8
  2. web/static/app.js +33 -18
  3. web/static/progress.js +34 -9
  4. web/static/runs.js +188 -46
web/static/app.css CHANGED
@@ -900,24 +900,27 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
900
  .endpoint-params{margin:8px 0 0;color:#667085;font-size:11px;line-height:1.35}
901
  .compact-empty{font-size:11px;padding:6px 0;color:#667085}
902
 
903
- /* v101: compact Active Run trace/document dock */
904
  .run-documents-card{background:linear-gradient(180deg,#fff 0%,#fbfdff 100%)}
905
  .run-documents-card .card-title-row{align-items:center}
906
  .run-documents-card .card-title-row .muted{font-size:10.5px}
907
- .run-documents-dock{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:8px}
908
- .run-doc-btn{display:grid;grid-template-columns:32px minmax(0,1fr);gap:8px;align-items:center;border:1px solid var(--line);border-radius:12px;background:#fff;padding:9px 10px;color:#344054;text-align:left;min-height:54px;transition:border-color .16s ease,box-shadow .16s ease,background .16s ease,transform .16s ease}
909
  .run-doc-btn.ready:hover{border-color:#93c5fd;background:#fff;box-shadow:0 8px 18px rgba(16,24,40,.08);transform:translateY(-1px)}
910
- .run-doc-btn.pending{opacity:.52;filter:grayscale(.25);cursor:not-allowed;background:#f8fafc;border-style:dashed}
911
- .run-doc-btn .doc-icon{width:32px;height:32px;border-radius:10px;display:grid;place-items:center;background:#f8fafc;border:1px solid #eef2f7;font-size:15px}
912
- .run-doc-btn strong{display:block;font-size:12px;line-height:1.15;color:#101828;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
913
- .run-doc-btn em{display:block;font-style:normal;font-size:10.5px;line-height:1.25;color:#667085;margin-top:2px}
 
 
914
  .run-doc-btn.raw.ready{border-color:#fed7aa;background:linear-gradient(180deg,#fffaf3 0%,#fff 100%)}
915
  .run-doc-btn.raw.ready .doc-icon{background:#fff7ed;border-color:#fed7aa}
916
  .run-doc-btn.safe.ready{border-color:#c7d2fe;background:linear-gradient(180deg,#f8fbff 0%,#fff 100%)}
917
  .run-doc-btn.safe.ready .doc-icon{background:#eef2ff;border-color:#c7d2fe}
918
  .run-doc-btn.warn.ready{border-color:#fed7aa;background:linear-gradient(180deg,#fff7ed 0%,#fff 100%)}
919
  .run-doc-btn.warn.ready strong{color:#9a3412}
920
- @media (max-width:720px){.run-documents-dock{grid-template-columns:1fr}}
 
921
 
922
  /* v102: narrow-safe run explorer, point-based validation progress */
923
  .runs-table{min-width:0}
@@ -1272,6 +1275,11 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
1272
  .recovery-meta strong{display:block;margin-top:2px;font-size:11.5px;color:#344054;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1273
  .recovery-evidence{margin:0;padding-left:18px;color:#475467;font-size:11.5px;line-height:1.4}
1274
  .recovery-next{margin:0;border:1px dashed #c7d2fe;background:#f8fbff;color:#3730a3;border-radius:11px;padding:8px 9px;font-size:11.5px;line-height:1.35}
 
 
 
 
 
1275
  .run-doc-btn .doc-icon{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-weight:900}
1276
  @media (max-width:720px){.recovery-ladder,.recovery-meta{grid-template-columns:1fr 1fr}}
1277
  @media (max-width:480px){.recovery-ladder,.recovery-meta{grid-template-columns:1fr}}
@@ -1617,3 +1625,290 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
1617
  .step.trace-post-failure.status-muted:before{
1618
  background:var(--red);
1619
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
900
  .endpoint-params{margin:8px 0 0;color:#667085;font-size:11px;line-height:1.35}
901
  .compact-empty{font-size:11px;padding:6px 0;color:#667085}
902
 
903
+ /* v159: horizontal source-document dock */
904
  .run-documents-card{background:linear-gradient(180deg,#fff 0%,#fbfdff 100%)}
905
  .run-documents-card .card-title-row{align-items:center}
906
  .run-documents-card .card-title-row .muted{font-size:10.5px}
907
+ .run-documents-dock{display:flex;align-items:stretch;gap:8px;margin-top:8px;overflow-x:auto;overflow-y:hidden;padding:2px 2px 8px;scrollbar-width:thin;scroll-snap-type:x proximity}
908
+ .run-doc-btn{position:relative;flex:0 0 86px;display:grid;grid-template-rows:34px auto auto;justify-items:center;align-content:start;gap:5px;border:1px solid var(--line);border-radius:13px;background:#fff;padding:9px 8px 8px;color:#344054;text-align:center;min-height:92px;text-decoration:none;scroll-snap-align:start;transition:border-color .16s ease,box-shadow .16s ease,background .16s ease,transform .16s ease}
909
  .run-doc-btn.ready:hover{border-color:#93c5fd;background:#fff;box-shadow:0 8px 18px rgba(16,24,40,.08);transform:translateY(-1px)}
910
+ .run-doc-btn.pending{opacity:.56;filter:grayscale(.25);cursor:not-allowed;background:#f8fafc;border-style:dashed}
911
+ .run-doc-btn.ready::after{content:"";position:absolute;top:7px;right:7px;width:7px;height:7px;border-radius:999px;background:#22c55e;box-shadow:0 0 0 2px #fff}
912
+ .run-doc-btn.pending::after{content:"";position:absolute;top:7px;right:7px;width:7px;height:7px;border-radius:999px;background:#cbd5e1;box-shadow:0 0 0 2px #fff}
913
+ .run-doc-btn .doc-icon{width:34px;height:34px;border-radius:11px;display:grid;place-items:center;background:#f8fafc;border:1px solid #eef2f7;font-size:16px;line-height:1}
914
+ .run-doc-btn strong{display:block;width:100%;font-size:11.5px;line-height:1.12;color:#101828;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
915
+ .run-doc-btn em{display:block;width:100%;font-style:normal;font-size:9.5px;line-height:1.18;color:#667085;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
916
  .run-doc-btn.raw.ready{border-color:#fed7aa;background:linear-gradient(180deg,#fffaf3 0%,#fff 100%)}
917
  .run-doc-btn.raw.ready .doc-icon{background:#fff7ed;border-color:#fed7aa}
918
  .run-doc-btn.safe.ready{border-color:#c7d2fe;background:linear-gradient(180deg,#f8fbff 0%,#fff 100%)}
919
  .run-doc-btn.safe.ready .doc-icon{background:#eef2ff;border-color:#c7d2fe}
920
  .run-doc-btn.warn.ready{border-color:#fed7aa;background:linear-gradient(180deg,#fff7ed 0%,#fff 100%)}
921
  .run-doc-btn.warn.ready strong{color:#9a3412}
922
+ .run-documents-dock .empty{min-width:100%}
923
+ @media (max-width:420px){.run-doc-btn{flex-basis:78px;min-height:88px;padding-left:7px;padding-right:7px}.run-doc-btn strong{font-size:11px}.run-doc-btn em{font-size:9px}}
924
 
925
  /* v102: narrow-safe run explorer, point-based validation progress */
926
  .runs-table{min-width:0}
 
1275
  .recovery-meta strong{display:block;margin-top:2px;font-size:11.5px;color:#344054;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1276
  .recovery-evidence{margin:0;padding-left:18px;color:#475467;font-size:11.5px;line-height:1.4}
1277
  .recovery-next{margin:0;border:1px dashed #c7d2fe;background:#f8fbff;color:#3730a3;border-radius:11px;padding:8px 9px;font-size:11.5px;line-height:1.35}
1278
+ .recovery-docs{display:flex;align-items:center;gap:7px;flex-wrap:wrap;border:1px solid #e5e7eb;background:#fff;border-radius:11px;padding:8px 9px}
1279
+ .recovery-docs span{font-size:9.5px;text-transform:uppercase;letter-spacing:.06em;color:#94a3b8;font-weight:900;margin-right:2px}
1280
+ .recovery-docs a{font-size:11px;font-weight:850;text-decoration:none;border:1px solid #dbeafe;background:#eff6ff;color:#1d4ed8;border-radius:999px;padding:4px 8px}
1281
+ .recovery-docs a.warn{border-color:#fed7aa;background:#fff7ed;color:#c2410c}
1282
+ .recovery-docs a.raw{border-color:#fecdd3;background:#fff1f2;color:#be123c}
1283
  .run-doc-btn .doc-icon{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-weight:900}
1284
  @media (max-width:720px){.recovery-ladder,.recovery-meta{grid-template-columns:1fr 1fr}}
1285
  @media (max-width:480px){.recovery-ladder,.recovery-meta{grid-template-columns:1fr}}
 
1625
  .step.trace-post-failure.status-muted:before{
1626
  background:var(--red);
1627
  }
1628
+
1629
+ /* v153 — visual stability and premium polish */
1630
+ .loading-shimmer{
1631
+ position:relative;
1632
+ overflow:hidden;
1633
+ }
1634
+ .loading-shimmer::after{
1635
+ content:"";
1636
+ position:absolute;
1637
+ inset:0;
1638
+ transform:translateX(-100%);
1639
+ background:linear-gradient(90deg,transparent,rgba(255,255,255,.65),transparent);
1640
+ animation:shimmer-pass 1.3s ease-in-out infinite;
1641
+ }
1642
+ @keyframes shimmer-pass{100%{transform:translateX(100%)}}
1643
+ #activeLatencyCard.is-loading{
1644
+ opacity:.78;
1645
+ }
1646
+ .timeline.trace-failed .step.trace-breakpoint{
1647
+ transform:none;
1648
+ }
1649
+ .timeline.trace-failed .step.trace-breakpoint .step-dot,
1650
+ .timeline.trace-recovered .step.trace-recovered .step-dot{
1651
+ width:28px;
1652
+ height:28px;
1653
+ min-width:28px;
1654
+ }
1655
+ .timeline.trace-failed .step.trace-breakpoint:after,
1656
+ .timeline.trace-recovered .step.trace-recovered:after{
1657
+ top:2px;
1658
+ height:40px;
1659
+ }
1660
+ .endpoint-meta{
1661
+ display:flex;
1662
+ gap:8px;
1663
+ flex-wrap:wrap;
1664
+ margin-bottom:8px;
1665
+ }
1666
+ .endpoint-meta span{
1667
+ border:1px solid var(--rule);
1668
+ border-radius:999px;
1669
+ padding:3px 8px;
1670
+ background:#fff;
1671
+ color:var(--muted);
1672
+ font-size:11px;
1673
+ font-weight:800;
1674
+ }
1675
+ .endpoint-param span{
1676
+ display:block;
1677
+ opacity:.7;
1678
+ margin-top:2px;
1679
+ }
1680
+ .prescan-meta{
1681
+ display:grid!important;
1682
+ grid-template-columns:repeat(3,minmax(0,1fr));
1683
+ gap:8px!important;
1684
+ margin-bottom:8px;
1685
+ }
1686
+ .prescan-meta span{
1687
+ border:1px solid var(--rule);
1688
+ border-radius:12px;
1689
+ padding:8px 10px;
1690
+ background:#fff;
1691
+ }
1692
+ .prescan-meta em{
1693
+ display:block;
1694
+ font-size:10px;
1695
+ text-transform:uppercase;
1696
+ letter-spacing:.06em;
1697
+ color:var(--muted);
1698
+ font-style:normal;
1699
+ font-weight:900;
1700
+ }
1701
+ .prescan-meta strong{
1702
+ display:block;
1703
+ margin-top:3px;
1704
+ overflow:hidden;
1705
+ text-overflow:ellipsis;
1706
+ white-space:nowrap;
1707
+ }
1708
+
1709
+ /* v160 premium destructive action modal */
1710
+ .delete-run-modal[hidden]{display:none!important}
1711
+ .delete-run-modal{position:fixed;inset:0;z-index:1000;display:grid;place-items:center;padding:24px;opacity:0;pointer-events:none;transition:opacity .18s ease}
1712
+ .delete-run-modal.is-open{opacity:1;pointer-events:auto}
1713
+ .delete-run-backdrop{position:absolute;inset:0;background:rgba(15,23,42,.42);backdrop-filter:blur(10px)}
1714
+ .delete-run-card{position:relative;width:min(520px,calc(100vw - 32px));display:grid;gap:16px;padding:22px;border-radius:28px;border:1px solid rgba(255,255,255,.78);background:linear-gradient(180deg,rgba(255,255,255,.98),rgba(248,250,252,.96));box-shadow:0 30px 90px rgba(15,23,42,.25),0 1px 0 rgba(255,255,255,.8) inset;transform:translateY(8px) scale(.98);transition:transform .18s ease}
1715
+ .delete-run-modal.is-open .delete-run-card{transform:translateY(0) scale(1)}
1716
+ .delete-run-close{position:absolute;top:14px;right:14px;width:34px;height:34px;border-radius:999px;border:1px solid #e5e7eb;background:#fff;color:#667085;font-size:20px;line-height:1;display:grid;place-items:center;cursor:pointer}
1717
+ .delete-run-close:hover{background:#fff1f2;border-color:#fecdd3;color:#be123c}
1718
+ .delete-run-icon{width:52px;height:52px;border-radius:18px;display:grid;place-items:center;background:linear-gradient(180deg,#fff1f2,#ffe4e6);border:1px solid #fecdd3;color:#be123c;font-size:26px;box-shadow:0 10px 24px rgba(190,18,60,.12)}
1719
+ .delete-run-copy{display:grid;gap:6px;padding-right:28px}.delete-run-kicker{font-size:11px;font-weight:900;text-transform:uppercase;letter-spacing:.08em;color:#be123c}.delete-run-copy h3{margin:0;font-size:22px;line-height:1.15;color:#101828;letter-spacing:-.03em}.delete-run-copy p{margin:0;color:#475467;font-size:13px;line-height:1.45}
1720
+ .delete-run-meta{display:grid;gap:8px;padding:12px;border:1px solid #e5e7eb;border-radius:18px;background:#fff}.delete-run-meta span{display:grid;grid-template-columns:64px minmax(0,1fr);gap:10px;align-items:center;font-size:12px;color:#344054}.delete-run-meta strong{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:#98a2b3}.delete-run-meta code{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11.5px;color:#101828;background:#f8fafc;border:1px solid #eef2f7;border-radius:9px;padding:5px 7px}
1721
+ .delete-run-warning{border:1px solid #fed7aa;background:#fff7ed;color:#9a3412;border-radius:16px;padding:10px 12px;font-size:12px;line-height:1.4}.delete-run-warning[hidden]{display:none!important}
1722
+ .delete-run-actions{display:flex;justify-content:flex-end;gap:10px}.delete-run-actions .primary.danger{background:linear-gradient(180deg,#e11d48,#be123c);border-color:#be123c;color:#fff;box-shadow:0 12px 24px rgba(190,18,60,.22)}.delete-run-actions .primary.danger:hover{background:linear-gradient(180deg,#be123c,#9f1239)}
1723
+ @media (max-width:520px){.delete-run-modal{padding:14px}.delete-run-card{border-radius:22px;padding:18px}.delete-run-actions{display:grid;grid-template-columns:1fr}.delete-run-actions button{width:100%}.delete-run-meta span{grid-template-columns:1fr;gap:4px}}
1724
+
1725
+ /* v162: Latest events gets enough vertical room for live runs */
1726
+ .latest-events-card .activity-feed,
1727
+ .latest-events-card .activity-feed.show-all{
1728
+ max-height:354px;
1729
+ overflow-y:auto;
1730
+ overscroll-behavior:contain;
1731
+ }
1732
+
1733
+ /* v164 — premium agent timeline: precise milestones + clockwise grouped progress */
1734
+ .timeline{
1735
+ --tl-line:#d8dee9;
1736
+ --tl-muted:#98a2b3;
1737
+ --tl-ring-bg:#e5e7eb;
1738
+ display:grid!important;
1739
+ grid-auto-flow:column!important;
1740
+ grid-auto-columns:minmax(98px,1fr)!important;
1741
+ gap:0!important;
1742
+ align-items:start!important;
1743
+ padding:8px 2px 10px!important;
1744
+ overflow-x:auto!important;
1745
+ overflow-y:hidden!important;
1746
+ scroll-snap-type:x proximity;
1747
+ }
1748
+ .timeline .step{
1749
+ position:relative;
1750
+ display:grid;
1751
+ justify-items:center;
1752
+ gap:6px;
1753
+ min-width:98px!important;
1754
+ max-width:136px!important;
1755
+ padding:7px 8px 2px!important;
1756
+ scroll-snap-align:center;
1757
+ text-align:center;
1758
+ transition:opacity .18s ease, transform .18s ease, filter .18s ease;
1759
+ }
1760
+ .timeline .step:before{
1761
+ content:"";
1762
+ position:absolute;
1763
+ left:0;
1764
+ right:0;
1765
+ top:24px;
1766
+ height:2px;
1767
+ background:var(--tl-line);
1768
+ z-index:0;
1769
+ transition:background .22s ease;
1770
+ }
1771
+ .timeline .step:first-child:before{left:50%}
1772
+ .timeline .step:last-child:before{right:50%}
1773
+ .timeline .step-dot{
1774
+ position:relative;
1775
+ z-index:1;
1776
+ width:34px!important;
1777
+ height:34px!important;
1778
+ min-width:34px!important;
1779
+ border:0!important;
1780
+ border-radius:999px;
1781
+ display:grid;
1782
+ place-items:center;
1783
+ padding:3px!important;
1784
+ color:var(--timeline-fg,#475467)!important;
1785
+ background:
1786
+ conic-gradient(from -90deg,var(--timeline-ring,var(--blue)) 0 var(--step-progress,0%),var(--tl-ring-bg) var(--step-progress,0%) 100%)!important;
1787
+ box-shadow:0 1px 2px rgba(15,23,42,.08),0 0 0 1px rgba(15,23,42,.06) inset!important;
1788
+ transition:background .28s ease, transform .18s ease, box-shadow .18s ease, color .18s ease;
1789
+ }
1790
+ .timeline .step-dot span{
1791
+ position:relative;
1792
+ z-index:2;
1793
+ width:24px!important;
1794
+ height:24px!important;
1795
+ border-radius:999px;
1796
+ display:grid;
1797
+ place-items:center;
1798
+ margin:auto;
1799
+ background:#fff!important;
1800
+ color:inherit!important;
1801
+ font-size:11px;
1802
+ font-weight:900;
1803
+ line-height:1;
1804
+ box-shadow:0 0 0 1px rgba(15,23,42,.06) inset;
1805
+ }
1806
+ .timeline .step.is-grouped .step-dot::before{
1807
+ content:"";
1808
+ position:absolute;
1809
+ inset:-3px;
1810
+ border-radius:999px;
1811
+ border:1px solid rgba(15,23,42,.08);
1812
+ pointer-events:none;
1813
+ }
1814
+ .timeline .step-count{
1815
+ position:absolute;
1816
+ right:-8px;
1817
+ bottom:-5px;
1818
+ z-index:3;
1819
+ min-width:23px;
1820
+ height:15px;
1821
+ padding:0 5px;
1822
+ display:grid;
1823
+ place-items:center;
1824
+ border-radius:999px;
1825
+ border:1px solid rgba(15,23,42,.08);
1826
+ background:#fff;
1827
+ color:#667085;
1828
+ font-size:8.5px;
1829
+ font-style:normal;
1830
+ font-weight:900;
1831
+ letter-spacing:-.02em;
1832
+ box-shadow:0 2px 5px rgba(15,23,42,.08);
1833
+ }
1834
+ .timeline .step strong{
1835
+ display:block!important;
1836
+ max-width:100%;
1837
+ overflow:hidden;
1838
+ text-overflow:ellipsis;
1839
+ white-space:nowrap;
1840
+ color:#344054;
1841
+ font-size:10.75px!important;
1842
+ line-height:1.12;
1843
+ letter-spacing:-.01em;
1844
+ }
1845
+ .timeline .step small{
1846
+ display:block!important;
1847
+ max-width:100%;
1848
+ margin-top:2px;
1849
+ overflow:hidden;
1850
+ text-overflow:ellipsis;
1851
+ white-space:nowrap;
1852
+ color:#98a2b3;
1853
+ font-size:9px!important;
1854
+ line-height:1.1;
1855
+ }
1856
+ .timeline .step.status-done{--timeline-ring:var(--green);--timeline-fg:#047857}
1857
+ .timeline .step.status-running,.timeline .step.status-waiting{--timeline-ring:var(--blue);--timeline-fg:#2563eb}
1858
+ .timeline .step.status-fallback,.timeline .step.status-warn{--timeline-ring:var(--amber);--timeline-fg:#92400e;--tl-line:#fde68a}
1859
+ .timeline .step.status-failed{--timeline-ring:var(--red);--timeline-fg:#be123c;--tl-line:#fecdd3}
1860
+ .timeline .step.status-stopped,.timeline .step.status-after-fail{--timeline-ring:#64748b;--timeline-fg:#475569;--tl-line:#cbd5e1}
1861
+ .timeline .step.status-pending,.timeline .step.status-muted{--timeline-ring:#cbd5e1;--timeline-fg:#98a2b3;opacity:.72}
1862
+ .timeline .step.status-done:before{background:var(--green)!important}
1863
+ .timeline .step.status-running:before,.timeline .step.status-waiting:before{background:linear-gradient(90deg,var(--green) 0 50%,var(--tl-line) 50%)!important}
1864
+ .timeline .step.status-fallback:before,.timeline .step.status-warn:before{background:linear-gradient(90deg,var(--green) 0 50%,var(--amber) 50%)!important}
1865
+ .timeline .step.status-failed:before{background:linear-gradient(90deg,var(--green) 0 50%,var(--red) 50%)!important}
1866
+ .timeline .step.status-after-fail:before{background:var(--red)!important}
1867
+ .timeline .step.status-running .step-dot,
1868
+ .timeline .step.status-waiting .step-dot{
1869
+ animation:timeline-active-breathe 1.45s ease-in-out infinite!important;
1870
+ }
1871
+ .timeline .step.status-running .step-dot::after,
1872
+ .timeline .step.status-waiting .step-dot::after,
1873
+ .timeline .step.running .step-dot::after{
1874
+ inset:-8px!important;
1875
+ border-color:rgba(37,99,235,.2)!important;
1876
+ animation:timeline-soft-wave 1.45s ease-out infinite!important;
1877
+ }
1878
+ .timeline .step.status-changed .step-dot,
1879
+ .timeline .step .step-dot.dot-pop{
1880
+ animation:timeline-dot-pop .56s cubic-bezier(.2,.9,.2,1.2)!important;
1881
+ }
1882
+ .timeline.trace-failed .step.trace-breakpoint .step-dot,
1883
+ .timeline.trace-recovered .step.trace-recovered .step-dot{
1884
+ width:34px!important;
1885
+ height:34px!important;
1886
+ min-width:34px!important;
1887
+ }
1888
+ .timeline.trace-failed .step.trace-breakpoint:after,
1889
+ .timeline.trace-recovered .step.trace-recovered:after{
1890
+ display:none!important;
1891
+ }
1892
+ @keyframes timeline-active-breathe{
1893
+ 0%,100%{transform:translateY(0) scale(1);box-shadow:0 1px 2px rgba(15,23,42,.08),0 0 0 1px rgba(37,99,235,.08) inset}
1894
+ 50%{transform:translateY(-1px) scale(1.04);box-shadow:0 8px 18px rgba(37,99,235,.13),0 0 0 1px rgba(37,99,235,.12) inset}
1895
+ }
1896
+ @keyframes timeline-soft-wave{
1897
+ 0%{transform:scale(.78);opacity:.85}
1898
+ 72%{transform:scale(1.28);opacity:0}
1899
+ 100%{transform:scale(1.28);opacity:0}
1900
+ }
1901
+ @keyframes timeline-dot-pop{
1902
+ 0%{transform:scale(.82)}
1903
+ 60%{transform:scale(1.12)}
1904
+ 100%{transform:scale(1)}
1905
+ }
1906
+ @media (prefers-reduced-motion:reduce){
1907
+ .timeline,.timeline *{scroll-behavior:auto!important;animation:none!important;transition:none!important}
1908
+ }
1909
+ @media (max-width:720px){
1910
+ .timeline{grid-auto-flow:row!important;grid-auto-columns:auto!important;grid-template-columns:1fr!important;overflow-x:hidden!important;gap:6px!important}
1911
+ .timeline .step{grid-template-columns:34px minmax(0,1fr);justify-items:start;text-align:left;max-width:none!important;width:100%!important;min-width:0!important}
1912
+ .timeline .step:before{left:24px!important;right:auto!important;top:0;bottom:0;width:2px;height:auto}
1913
+ .timeline .step strong,.timeline .step small{text-align:left}
1914
+ }
web/static/app.js CHANGED
@@ -1,4 +1,4 @@
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,runDetailCache:{},runDetailCacheTtlMs:300000};
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
 
@@ -168,8 +168,12 @@ function endpointDetails(detail={},endpointName=''){
168
  function renderEndpointParamChips(items=[]){
169
  if(!items.length)return '<span class="muted">No parameters discovered yet.</span>';
170
  return items.slice(0,10).map(item=>{
171
- const detail=[item.type,item.component,item.optional?'optional':''].filter(Boolean).join(' · ');
172
- return `<em title="${escapeHtml(detail||item.name)}"><strong>${escapeHtml(item.name)}</strong>${detail?`<span>${escapeHtml(detail)}</span>`:''}</em>`;
 
 
 
 
173
  }).join('')+(items.length>10?'<em><strong>…</strong></em>':'');
174
  }
175
  function renderValidationEndpoints(detail={}){
@@ -186,7 +190,8 @@ function renderValidationEndpoints(detail={}){
186
  root.dataset.signature=signature;
187
  const inputLine=`<div class="endpoint-io"><strong>Inputs</strong><span>${renderEndpointParamChips(details.inputs)}</span></div>`;
188
  const outputLine=details.outputs.length?`<div class="endpoint-io"><strong>Outputs</strong><span>${renderEndpointParamChips(details.outputs)}</span></div>`:'';
189
- root.innerHTML=`<div class="endpoint-chip-list">${chips}</div><div class="endpoint-info-card">${inputLine}${outputLine}${details.description?`<p>${escapeHtml(details.description)}</p>`:''}</div>`;
 
190
  }
191
  function setGeneratedSpaceLinksEnabled(links={},enabled=false){
192
  const space=enabled?links.target_space_url:'';
@@ -199,7 +204,7 @@ let operationTimer=null;function toastDuration(type='info'){if(type==='loading')
199
  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)}
200
  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}
201
  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')}
202
- 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')}}
203
  function setBucketFeedback(mode='checking',message='Checking private bucket access…'){
204
  const orb=$('bucketInlineOrb');
205
  if(orb)orb.className='bucket-status-orb '+mode;
@@ -253,8 +258,9 @@ function renderModelPreScan(scan=null){
253
  const recs=[...(scan.recommendations||[]).slice(0,2)];
254
  if(outputType)recs.unshift(`Expected output: ${outputType}`);
255
  if(signals){
 
256
  signals.hidden=false;
257
- 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>`:''}`;
258
  }
259
  if(ackWrap)ackWrap.hidden=!(verdict==='risky');
260
  }
@@ -290,11 +296,12 @@ function resetRunDetails(){
290
  if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;if(typeof resetProgressStability==='function')resetProgressStability(null);
291
  if(typeof activityFeedState!=='undefined'){activityFeedState.lastItems=[];activityFeedState.runId=null}
292
  for(const [id,value] of [
293
- ['detailRunId','—'],['detailJobStatus','—'],['detailGateStatus','—'],['detailSpace','—'],['detailHardware','—'],['detailTraces','—'],
294
  ['homeRunDetailStatus','No run'],['selectedModelId','—'],['progressTitle','No run selected'],['progressStatus','Idle'],
295
  ['currentStep','—'],['lastEvent','Select a run from the explorer or launch a new build.'],['elapsed','—'],['lastPolled','—'],
296
  ['progressPercent','0%']
297
  ])setText(id,value);
 
298
  const status=$('progressStatus');if(status)status.className='badge neutral';
299
  const fill=$('progressFill');if(fill){fill.style.width='0%';fill.classList.remove('is-running','progress-bump')}
300
  setQuickLinks({});
@@ -310,8 +317,8 @@ function resetRunDetails(){
310
  const blockers=$('selectedBlockers');if(blockers){blockers.hidden=true;blockers.innerHTML=''}
311
  for(const id of ['deleteActiveRun','activePrefillSpaceTest','deleteValidationRun']){const btn=$(id);if(btn)btn.disabled=true}
312
  renderTimeline(DEFAULT_TIMELINE,'idle');
313
- } function prepareNewBuild(){if(state.poll){clearInterval(state.poll);state.poll=null}state.runId=null;state.activeRunProgress=null;state.selectedRunDetail=null;state.activeLinks={};state.pollErrors=0;clearSavedRun();document.querySelectorAll('.run-row').forEach(r=>r.classList.remove('selected'));setBuildModeNew();resetRunDetails();renderTimeline(DEFAULT_TIMELINE);setText('progressPercent','0%');const fill=$('progressFill');if(fill)fill.style.width='0%';setText('currentStep','Ready');setText('lastEvent','Choose a model card and launch a build.');setText('elapsed','—');if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;setText('lastPolled','—');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')}
314
- function setActiveRun(result){if(state.poll){clearInterval(state.poll);state.poll=null}resetRunDetails();state.runId=result.run_id;state.spaceLinksUnlockedFor=null;if(Array.isArray(state.runsCache)&&typeof sortRunsNewestFirst==='function'){state.runsCache=sortRunsNewestFirst([{...result,run_id:result.run_id,status:result.status||'running',created_at:result.created_at||new Date().toISOString()},...state.runsCache.filter(r=>r.run_id!==result.run_id)]);renderRunsFromCache();}if(result.bucket_source)state.bucketSource=result.bucket_source;setText('detailRunId',result.run_id);setText('detailJobStatus',result.status||'RUNNING');setText('detailGateStatus',result.kind||'running');setText('detailSpace',result.target_space||'—');setText('detailHardware',result.selected_hardware||result.preferred_space_hardware||'—');renderActiveRunMeta({summary:result,state:result,launch:result});renderProgress({run_id:result.run_id,status:result.status||'running',progress:1,current_step_label:'Launching build Job…',events:[{step:'launch',status:'started',message:'Build Job launched; waiting for first worker event.'}],summary:result,state:result});setText('homeRunDetailStatus',result.status||'Running');setBuildModeInspecting(result.run_id);setQuickLinks({...result.links,bucket_source:result.bucket_source,run_id:result.run_id,job_id:result.job_id,job_owner:state.user?.username,target_space_url:result.target_space_url},result);saveActiveRun();markPendingActiveFields();startPolling(result.run_id)}
315
  function startPolling(runId,options={}){
316
  if(!runId)return;
317
  const sameRun=state.runId===runId;
@@ -429,7 +436,7 @@ function renderValidationStatus(data={}){
429
  if(card)card.classList.toggle('is-validating',isRunning);
430
  const runId=data.run_id||summary.run_id||state.validationRunId||'—';
431
  setText('spaceTestOutput',runId);
432
- setText('lastPolled',formatTime(data.updated_at||summary.updated_at||data.state?.updated_at||data.created_at||summary.created_at||new Date().toISOString()));
433
  const latency=firstPositiveNumber(smoke.latency_seconds,smoke.observed_latency_seconds,summary.latency_seconds,summary.observed_latency_seconds,data.latency_seconds,data.observed_latency_seconds);
434
  setText('spaceTestLatency',latency?formatLatencySeconds(latency):'—');
435
  setText('spaceTestVerdict',badgeLabel(status));
@@ -511,7 +518,7 @@ function prepareValidationFromActiveRun(){
511
  function prepareValidationFromSelectedRun(){
512
  applyValidationPrefill(state.selectedRunDetail||{},'selected Space');
513
  }
514
- async function launchBuild(){const formError=validateBuildForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching build Job…');setButtonBusy('launchBuild',true,'Launching…');try{setText('hardwarePlan',hardwarePlanLabel());const payload={bucket_name:state.bucketName,run_id:formValue('runId')||undefined,model_id:formValue('modelId'),target_space_name:formValue('targetSpace')||undefined,pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.checked??true,expected_output_type:formValue('buildExpectedOutput')};const result=await apiPost('/api/build',payload);setActiveRun(result);showMessage(`Build launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchBuild',false)}}
515
  async function launchValidate(){const formError=validateValidationForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching validation Job…');setButtonBusy('launchValidate',true,'Launching…');try{const args=parseJsonField('testArgs',[]);const kwargs=parseJsonField('testKwargs',{});const payload={bucket_name:state.bucketName,target_space_id:formValue('validateSpace'),api_name:normalizeApiName(formValue('validateApi')||'/generate'),expected_output_type:formValue('expectedOutput')||'any',test_args_json:prettyJson(args),test_kwargs_json:prettyJson(kwargs),live_timeout_seconds:Number(formValue('validateTimeout')||1800)};const result=await apiPost('/api/validate',payload);renderValidationLaunchResult(result);startValidationPolling(result.run_id);showMessage(`Validation launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchValidate',false)}}
516
  async function refreshRunReportPreview(runId){
517
  if(!runId)return;
@@ -561,19 +568,29 @@ function readRunLatencyPayload(p={}){
561
  function renderActiveLatency(p={}){
562
  const card=$('activeLatencyCard');
563
  if(!card)return;
 
 
564
  const {latency,duration,source}=readRunLatencyPayload(p);
565
  const hasLatency=Number.isFinite(latency)&&latency>0;
566
  const hasDuration=Number.isFinite(duration)&&duration>0;
567
- setText('activeLatencyValue',hasLatency?formatLatencySeconds(latency):'—');
568
- setText('activeZeroGpuDuration',hasDuration?fmtSeconds(Math.round(duration)):'—');
569
- setText('activeLatencySource',hasLatency?(source||'live smoke test'):'measured when smoke test passes');
 
 
 
 
 
 
 
570
  const note=$('activeLatencyNote');
571
  if(note){
572
- note.textContent=hasDuration
573
  ? 'This measured latency is normally used to tune the `@spaces.GPU(duration=...)` decorator for ZeroGPU when the generated Space uses ZeroGPU.'
574
  : 'When available, the live smoke-test latency is used to recommend the `@spaces.GPU(duration=...)` value for ZeroGPU apps.';
575
  }
576
- card.classList.toggle('has-latency',hasLatency);
 
577
  }
578
  function runHardwareMeta(p={}){
579
  const summary=p.summary||{};
@@ -653,8 +670,6 @@ function bindActions(){
653
  refreshRuns:()=>loadRuns(true),
654
  refreshRunsHome:()=>loadRuns(true),
655
  cancelRun:cancelActiveRun,
656
- deleteActiveRun:()=>deleteRunById(state.runId,{kind:'active',detail:state.selectedRunDetail||state.activeRunProgress||{}}),
657
- deleteValidationRun:()=>deleteRunById(state.validationRunId,{kind:'validation',detail:state.validationDetail||{}}),
658
  };
659
  Object.entries(actions).forEach(([id,handler])=>bindOnce($(id),'click',`action${id}`,handler));
660
  bindOnce($('modelId'),'input','modelPreScanReset',resetModelPreScan);
 
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,runDetailCache:{},runDetailCacheTtlMs:300000,deleteModalPromise:null,deleteModalRunId:null,deletingRuns:{}};
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
 
 
168
  function renderEndpointParamChips(items=[]){
169
  if(!items.length)return '<span class="muted">No parameters discovered yet.</span>';
170
  return items.slice(0,10).map(item=>{
171
+ const name=item.name||item.label||item.id||'param';
172
+ const type=item.type||item.datatype||'';
173
+ const component=item.component||item.component_type||item.rendered_component||'';
174
+ const required=item.optional?'optional':'required';
175
+ const detail=[type,component,required].filter(Boolean).join(' · ');
176
+ return `<em class="endpoint-param" title="${escapeHtml(detail||name)}"><strong>${escapeHtml(name)}</strong>${detail?`<span>${escapeHtml(detail)}</span>`:''}</em>`;
177
  }).join('')+(items.length>10?'<em><strong>…</strong></em>':'');
178
  }
179
  function renderValidationEndpoints(detail={}){
 
190
  root.dataset.signature=signature;
191
  const inputLine=`<div class="endpoint-io"><strong>Inputs</strong><span>${renderEndpointParamChips(details.inputs)}</span></div>`;
192
  const outputLine=details.outputs.length?`<div class="endpoint-io"><strong>Outputs</strong><span>${renderEndpointParamChips(details.outputs)}</span></div>`:'';
193
+ const meta=`<div class="endpoint-meta"><span>${escapeHtml(selected)}</span><span>${details.inputs.length} input${details.inputs.length===1?'':'s'}</span><span>${details.outputs.length} output${details.outputs.length===1?'':'s'}</span></div>`;
194
+ root.innerHTML=`<div class="endpoint-chip-list">${chips}</div><div class="endpoint-info-card">${meta}${inputLine}${outputLine}${details.description?`<p>${escapeHtml(details.description)}</p>`:''}</div>`;
195
  }
196
  function setGeneratedSpaceLinksEnabled(links={},enabled=false){
197
  const space=enabled?links.target_space_url:'';
 
204
  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)}
205
  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}
206
  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')}
207
+ function setButtonBusy(id,isBusy,busyLabel){const btn=$(id);if(!btn)return;if(isBusy){if(!btn.classList.contains('is-busy')&&!btn.dataset.oldText)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;delete btn.dataset.oldText}if(id==='launchBuild')setBuildLaunchEnabled();else btn.disabled=state.busy||btn.dataset.requiresBucket==='true'&&!state.bucketReady;btn.classList.remove('is-busy')}}
208
  function setBucketFeedback(mode='checking',message='Checking private bucket access…'){
209
  const orb=$('bucketInlineOrb');
210
  if(orb)orb.className='bucket-status-orb '+mode;
 
258
  const recs=[...(scan.recommendations||[]).slice(0,2)];
259
  if(outputType)recs.unshift(`Expected output: ${outputType}`);
260
  if(signals){
261
+ const modelMeta=[scan.pipeline_tag?['Pipeline',scan.pipeline_tag]:null,scan.library_name?['Library',scan.library_name]:null,outputType?['Output',outputType]:null].filter(Boolean);
262
  signals.hidden=false;
263
+ signals.innerHTML=`<div class="prescan-meta">${modelMeta.map(([k,v])=>`<span><em>${escapeHtml(k)}</em><strong>${escapeHtml(String(v))}</strong></span>`).join('')}</div>${good.length?`<div><strong>Good signals</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>`:''}`;
264
  }
265
  if(ackWrap)ackWrap.hidden=!(verdict==='risky');
266
  }
 
296
  if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;if(typeof resetProgressStability==='function')resetProgressStability(null);
297
  if(typeof activityFeedState!=='undefined'){activityFeedState.lastItems=[];activityFeedState.runId=null}
298
  for(const [id,value] of [
299
+ ['detailRunId','—'],['detailJobStatus','—'],['detailGateStatus','—'],['detailSpace','—'],['detailHardware','—'],['detailTargetGpu','—'],['detailFallbackGpu','—'],['detailExpectedOutput','—'],['detailTraces','—'],
300
  ['homeRunDetailStatus','No run'],['selectedModelId','—'],['progressTitle','No run selected'],['progressStatus','Idle'],
301
  ['currentStep','—'],['lastEvent','Select a run from the explorer or launch a new build.'],['elapsed','—'],['lastPolled','—'],
302
  ['progressPercent','0%']
303
  ])setText(id,value);
304
+ setText('activeLatencyValue','—');setText('activeZeroGpuDuration','—');setText('activeLatencySource','waiting for live smoke test');const latencyCard=$('activeLatencyCard');if(latencyCard){latencyCard.classList.remove('has-latency');latencyCard.classList.add('is-loading')}
305
  const status=$('progressStatus');if(status)status.className='badge neutral';
306
  const fill=$('progressFill');if(fill){fill.style.width='0%';fill.classList.remove('is-running','progress-bump')}
307
  setQuickLinks({});
 
317
  const blockers=$('selectedBlockers');if(blockers){blockers.hidden=true;blockers.innerHTML=''}
318
  for(const id of ['deleteActiveRun','activePrefillSpaceTest','deleteValidationRun']){const btn=$(id);if(btn)btn.disabled=true}
319
  renderTimeline(DEFAULT_TIMELINE,'idle');
320
+ } function prepareNewBuild(){if(state.poll){clearInterval(state.poll);state.poll=null}state.runId=null;state.activeRunProgress=null;state.selectedRunDetail=null;state.documentCache={};state.latencyCache={};state.activeLinks={};state.pollErrors=0;clearSavedRun();document.querySelectorAll('.run-row').forEach(r=>r.classList.remove('selected'));setBuildModeNew();resetRunDetails();renderTimeline(DEFAULT_TIMELINE);setText('progressPercent','0%');const fill=$('progressFill');if(fill)fill.style.width='0%';setText('currentStep','Ready');setText('lastEvent','Choose a model card and launch a build.');setText('elapsed','—');if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;setText('lastPolled','—');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')}
321
+ function setActiveRun(result){if(state.poll){clearInterval(state.poll);state.poll=null}resetRunDetails();state.documentCache={};state.latencyCache={};state.runId=result.run_id;state.spaceLinksUnlockedFor=null;if(Array.isArray(state.runsCache)&&typeof sortRunsNewestFirst==='function'){state.runsCache=sortRunsNewestFirst([{...result,run_id:result.run_id,status:result.status||'running',created_at:result.created_at||new Date().toISOString()},...state.runsCache.filter(r=>r.run_id!==result.run_id)]);renderRunsFromCache();}if(result.bucket_source)state.bucketSource=result.bucket_source;setText('detailRunId',result.run_id);setText('detailJobStatus',result.status||'RUNNING');setText('detailGateStatus',result.kind||'running');setText('detailSpace',result.target_space||'—');setText('detailHardware',result.selected_hardware||result.preferred_space_hardware||'—');renderActiveRunMeta({summary:result,state:result,launch:result});renderProgress({run_id:result.run_id,status:result.status||'running',progress:1,current_step_label:'Launching build Job…',events:[{step:'launch',status:'started',message:'Build Job launched; waiting for first worker event.'}],summary:result,state:result});setText('homeRunDetailStatus',result.status||'Running');setBuildModeInspecting(result.run_id);setQuickLinks({...result.links,bucket_source:result.bucket_source,run_id:result.run_id,job_id:result.job_id,job_owner:state.user?.username,target_space_url:result.target_space_url},result);saveActiveRun();markPendingActiveFields();startPolling(result.run_id)}
322
  function startPolling(runId,options={}){
323
  if(!runId)return;
324
  const sameRun=state.runId===runId;
 
436
  if(card)card.classList.toggle('is-validating',isRunning);
437
  const runId=data.run_id||summary.run_id||state.validationRunId||'—';
438
  setText('spaceTestOutput',runId);
439
+ setText('validationStartedAt',formatTime(data.updated_at||summary.updated_at||data.state?.updated_at||data.created_at||summary.created_at||new Date().toISOString()));
440
  const latency=firstPositiveNumber(smoke.latency_seconds,smoke.observed_latency_seconds,summary.latency_seconds,summary.observed_latency_seconds,data.latency_seconds,data.observed_latency_seconds);
441
  setText('spaceTestLatency',latency?formatLatencySeconds(latency):'—');
442
  setText('spaceTestVerdict',badgeLabel(status));
 
518
  function prepareValidationFromSelectedRun(){
519
  applyValidationPrefill(state.selectedRunDetail||{},'selected Space');
520
  }
521
+ 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,model_id:formValue('modelId'),target_space_name:formValue('targetSpace')||undefined,pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.checked??true,try_zero_gpu_first:$('tryZeroGpu')?.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)}}
522
  async function launchValidate(){const formError=validateValidationForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching validation Job…');setButtonBusy('launchValidate',true,'Launching…');try{const args=parseJsonField('testArgs',[]);const kwargs=parseJsonField('testKwargs',{});const payload={bucket_name:state.bucketName,target_space_id:formValue('validateSpace'),api_name:normalizeApiName(formValue('validateApi')||'/generate'),expected_output_type:formValue('expectedOutput')||'any',test_args_json:prettyJson(args),test_kwargs_json:prettyJson(kwargs),live_timeout_seconds:Number(formValue('validateTimeout')||1800)};const result=await apiPost('/api/validate',payload);renderValidationLaunchResult(result);startValidationPolling(result.run_id);showMessage(`Validation launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchValidate',false)}}
523
  async function refreshRunReportPreview(runId){
524
  if(!runId)return;
 
568
  function renderActiveLatency(p={}){
569
  const card=$('activeLatencyCard');
570
  if(!card)return;
571
+ const runId=String(p.run_id||p.summary?.run_id||state.runId||'');
572
+ state.latencyCache=state.latencyCache||{};
573
  const {latency,duration,source}=readRunLatencyPayload(p);
574
  const hasLatency=Number.isFinite(latency)&&latency>0;
575
  const hasDuration=Number.isFinite(duration)&&duration>0;
576
+ const cached=state.latencyCache[runId]||{};
577
+ const stableLatency=hasLatency?latency:cached.latency;
578
+ const stableDuration=hasDuration?duration:cached.duration;
579
+ const stableSource=hasLatency?(source||'live smoke test'):(cached.source||'measured when smoke test passes');
580
+ if(hasLatency||hasDuration)state.latencyCache[runId]={latency:stableLatency,duration:stableDuration,source:stableSource};
581
+ const finalHasLatency=Number.isFinite(stableLatency)&&stableLatency>0;
582
+ const finalHasDuration=Number.isFinite(stableDuration)&&stableDuration>0;
583
+ setText('activeLatencyValue',finalHasLatency?formatLatencySeconds(stableLatency):'—');
584
+ setText('activeZeroGpuDuration',finalHasDuration?fmtSeconds(Math.round(stableDuration)):'—');
585
+ setText('activeLatencySource',stableSource);
586
  const note=$('activeLatencyNote');
587
  if(note){
588
+ note.textContent=finalHasDuration
589
  ? 'This measured latency is normally used to tune the `@spaces.GPU(duration=...)` decorator for ZeroGPU when the generated Space uses ZeroGPU.'
590
  : 'When available, the live smoke-test latency is used to recommend the `@spaces.GPU(duration=...)` value for ZeroGPU apps.';
591
  }
592
+ card.classList.toggle('has-latency',finalHasLatency);
593
+ card.classList.toggle('is-loading',!finalHasLatency);
594
  }
595
  function runHardwareMeta(p={}){
596
  const summary=p.summary||{};
 
670
  refreshRuns:()=>loadRuns(true),
671
  refreshRunsHome:()=>loadRuns(true),
672
  cancelRun:cancelActiveRun,
 
 
673
  };
674
  Object.entries(actions).forEach(([id,handler])=>bindOnce($(id),'click',`action${id}`,handler));
675
  bindOnce($('modelId'),'input','modelPreScanReset',resetModelPreScan);
web/static/progress.js CHANGED
@@ -12,18 +12,18 @@ const WORKER_STEP_ORDER=[
12
  const COMPACT_TIMELINE_GROUPS=[
13
  {id:'bootstrap',label:'Bootstrap',display_label:'Start',steps:['bootstrap']},
14
  {id:'dependencies',label:'Dependencies',display_label:'Dependencies',steps:['dependencies']},
15
- {id:'auth_model',label:'Auth + model',display_label:'Model card',steps:['auth','model_analysis']},
16
  {id:'workspace_node',label:'Workspace/node',display_label:'Workspace',steps:['workspace','node']},
17
  {id:'pi_setup',label:'Pi setup',display_label:'Agent setup',steps:['pi_install','pi_config']},
18
  {id:'pi_run',label:'Pi run',display_label:'Agent build',steps:['pi_run']},
19
  {id:'pi_verify',label:'Pi verify',display_label:'Agent review',steps:['pi_model_resolution','pi_verification','metadata_sanitize','requirements_sanitize']},
20
  {id:'hardware',label:'Hardware',display_label:'Hardware',steps:['hardware_strategy','create_space_hardware']},
21
- {id:'create_space',label:'Create Space',display_label:'Create Space',steps:['create_space']},
22
  {id:'upload_runtime',label:'Upload/runtime',display_label:'Deploy',steps:['upload_files','space_runtime','space_logs','live_wait']},
23
  {id:'validation',label:'Validation',display_label:'Live test',steps:['api_validation','generation_smoke','inference_gate']},
24
  {id:'repair',label:'Repair recovery',display_label:'Recovery',steps:['failure_detected','failure_diagnosis','pi_diagnosis','repair_decision','wait_for_logs','factory_rebuild','repair','repair_diagnosis','repair_brief','repair_plan','repair_patch','repair_upload','repair_validation','technical_blocker','manual_hardware_required']},
25
  {id:'report',label:'Report',display_label:'Report',steps:['report_write','done']},
26
- {id:'failure',label:'Failure',display_label:'Failure',steps:['failure']}
27
  ];
28
 
29
  const progressMotionState={lastProgress:null,lastStatus:null,lastStep:null};
@@ -322,13 +322,26 @@ function ensureTimelineStepVisible(stepKey,opts={}){
322
  }
323
  root.dataset.visibleStep=stepKey;
324
  }
 
 
 
 
 
 
 
 
 
325
  function visibleCompactTimelineGroups(items=[],overallStatus=''){
326
  const byStep=new Map((items||[]).map(item=>[String(item.step||item.id||item.label||''),item]));
327
  const overall=visualStatusClass(overallStatus);
 
328
  return COMPACT_TIMELINE_GROUPS.filter(group=>{
329
  const hasItem=group.steps.some(step=>byStep.has(step));
330
  if(group.id==='repair')return hasItem;
331
- if(group.id==='failure')return hasItem||overall==='error';
 
 
 
332
  return true;
333
  });
334
  }
@@ -362,6 +375,8 @@ function compactTimelineItems(items=[],overallStatus=''){
362
  else if(activeIndex>=0)percent=Math.round(((activeIndex+0.45)/group.steps.length)*100);
363
  else if(doneCount>0)percent=Math.round((doneCount/group.steps.length)*100);
364
  if(terminalRaw.has(status))percent=Math.max(percent,doneCount?Math.round((doneCount/group.steps.length)*100):8);
 
 
365
  return {
366
  step:group.id,
367
  id:group.id,
@@ -370,9 +385,12 @@ function compactTimelineItems(items=[],overallStatus=''){
370
  full_steps:group.steps,
371
  done_count:doneCount,
372
  total_count:group.steps.length,
373
- percent:Math.max(0,Math.min(100,percent)),
 
374
  active_substep:activeSubstep,
375
- active_substep_label:workerStepDisplayLabel(activeSubstep),
 
 
376
  reason
377
  };
378
  });
@@ -462,12 +480,15 @@ function renderTimeline(items,overallStatus=''){
462
  const label=st.replace(/^status-/,'');
463
  const percent=Number(x.percent||0);
464
  const friendlySubstep=x.active_substep_label||workerStepDisplayLabel(x.active_substep);
465
- const meta=x.total_count>1?(x.status==='done'?'done':(friendlySubstep?friendlySubstep:label)):label;
466
  const traceClass=traceClassForStep(trace,i);
467
  const reason=reasonForTimelineItem(x);
468
  const reasonLine=reason?`<small class="step-reason">${escapeHtml(reason)}</small>`:'';
469
- const title=reason?` title="${escapeHtml(reason)}"`:'';
470
- return `<div class="step ${st} ${traceClass}${changed?' status-changed':''}" data-step="${escapeHtml(key)}" data-status="${label}" style="--step-progress:${percent}%"${title}><div class="step-dot"><span>${timelineIcon(rawStatus)}</span></div><div><strong>${escapeHtml(x.label||`Step ${i+1}`)}</strong><small>${escapeHtml(meta)}</small>${reasonLine}</div></div>`;
 
 
 
471
  }).join('');
472
  Object.keys(timelineRenderState).forEach(k=>delete timelineRenderState[k]);
473
  Object.assign(timelineRenderState,nextState);
@@ -794,3 +815,7 @@ function renderProgress(p){
794
  // Legacy test anchors retained for timeline invariants:
795
  // currentStepLabel:activeItem?.label
796
  // x.done_count||0
 
 
 
 
 
12
  const COMPACT_TIMELINE_GROUPS=[
13
  {id:'bootstrap',label:'Bootstrap',display_label:'Start',steps:['bootstrap']},
14
  {id:'dependencies',label:'Dependencies',display_label:'Dependencies',steps:['dependencies']},
15
+ {id:'auth_model',label:'Auth + model',display_label:'Model access',steps:['auth','model_analysis']},
16
  {id:'workspace_node',label:'Workspace/node',display_label:'Workspace',steps:['workspace','node']},
17
  {id:'pi_setup',label:'Pi setup',display_label:'Agent setup',steps:['pi_install','pi_config']},
18
  {id:'pi_run',label:'Pi run',display_label:'Agent build',steps:['pi_run']},
19
  {id:'pi_verify',label:'Pi verify',display_label:'Agent review',steps:['pi_model_resolution','pi_verification','metadata_sanitize','requirements_sanitize']},
20
  {id:'hardware',label:'Hardware',display_label:'Hardware',steps:['hardware_strategy','create_space_hardware']},
21
+ {id:'create_space',label:'Create Space',display_label:'Space',steps:['create_space']},
22
  {id:'upload_runtime',label:'Upload/runtime',display_label:'Deploy',steps:['upload_files','space_runtime','space_logs','live_wait']},
23
  {id:'validation',label:'Validation',display_label:'Live test',steps:['api_validation','generation_smoke','inference_gate']},
24
  {id:'repair',label:'Repair recovery',display_label:'Recovery',steps:['failure_detected','failure_diagnosis','pi_diagnosis','repair_decision','wait_for_logs','factory_rebuild','repair','repair_diagnosis','repair_brief','repair_plan','repair_patch','repair_upload','repair_validation','technical_blocker','manual_hardware_required']},
25
  {id:'report',label:'Report',display_label:'Report',steps:['report_write','done']},
26
+ {id:'failure',label:'Failure',display_label:'Failure',terminal_only:true,steps:['failure']}
27
  ];
28
 
29
  const progressMotionState={lastProgress:null,lastStatus:null,lastStep:null};
 
322
  }
323
  root.dataset.visibleStep=stepKey;
324
  }
325
+ function hasSpecificTimelineProblem(items=[],overallStatus=''){
326
+ const byStep=new Map((items||[]).map(item=>[String(item.step||item.id||item.label||''),item]));
327
+ const overall=visualStatusClass(overallStatus);
328
+ return COMPACT_TIMELINE_GROUPS.some(group=>{
329
+ if(group.id==='failure')return false;
330
+ if(group.id==='repair')return group.steps.some(step=>byStep.has(step))&&overall==='error';
331
+ return group.steps.some(step=>isFailureStatus(byStep.get(step)?.status))||failedEventInfoForGroup(group);
332
+ });
333
+ }
334
  function visibleCompactTimelineGroups(items=[],overallStatus=''){
335
  const byStep=new Map((items||[]).map(item=>[String(item.step||item.id||item.label||''),item]));
336
  const overall=visualStatusClass(overallStatus);
337
+ const specificProblem=hasSpecificTimelineProblem(items,overallStatus);
338
  return COMPACT_TIMELINE_GROUPS.filter(group=>{
339
  const hasItem=group.steps.some(step=>byStep.has(step));
340
  if(group.id==='repair')return hasItem;
341
+ // Failure is a terminal fallback anchor, not a normal product milestone.
342
+ // Prefer marking the precise stage that failed; only show Failure when the
343
+ // run has no better failure/recovery context.
344
+ if(group.id==='failure')return hasItem&&!specificProblem;
345
  return true;
346
  });
347
  }
 
375
  else if(activeIndex>=0)percent=Math.round(((activeIndex+0.45)/group.steps.length)*100);
376
  else if(doneCount>0)percent=Math.round((doneCount/group.steps.length)*100);
377
  if(terminalRaw.has(status))percent=Math.max(percent,doneCount?Math.round((doneCount/group.steps.length)*100):8);
378
+ const clampedPercent=Math.max(0,Math.min(100,percent));
379
+ const substepLabel=workerStepDisplayLabel(activeSubstep);
380
  return {
381
  step:group.id,
382
  id:group.id,
 
385
  full_steps:group.steps,
386
  done_count:doneCount,
387
  total_count:group.steps.length,
388
+ percent:clampedPercent,
389
+ ring_progress:clampedPercent,
390
  active_substep:activeSubstep,
391
+ active_substep_label:substepLabel,
392
+ grouped:group.steps.length>1,
393
+ progress_label:group.steps.length>1?`${Math.min(group.steps.length,Math.max(doneCount,clampedPercent>=100?group.steps.length:Math.ceil((clampedPercent/100)*group.steps.length)))}/${group.steps.length}`:'',
394
  reason
395
  };
396
  });
 
480
  const label=st.replace(/^status-/,'');
481
  const percent=Number(x.percent||0);
482
  const friendlySubstep=x.active_substep_label||workerStepDisplayLabel(x.active_substep);
483
+ const meta=x.total_count>1?(x.status==='done'?'complete':(friendlySubstep?friendlySubstep:label)):label;
484
  const traceClass=traceClassForStep(trace,i);
485
  const reason=reasonForTimelineItem(x);
486
  const reasonLine=reason?`<small class="step-reason">${escapeHtml(reason)}</small>`:'';
487
+ const titleParts=[x.label||`Step ${i+1}`,meta,x.total_count>1?`${x.done_count||0}/${x.total_count} substeps`:reason].filter(Boolean);
488
+ const title=` title="${escapeHtml(titleParts.join(' · '))}"`;
489
+ const groupBadge=x.total_count>1?`<em class="step-count" aria-label="${escapeHtml((x.progress_label||'')+' substeps')}">${escapeHtml(x.progress_label||'')}</em>`:'';
490
+ const aria=`${x.label||`Step ${i+1}`}: ${label}${x.total_count>1?`, ${x.done_count||0} of ${x.total_count} substeps`:''}`;
491
+ return `<div class="step ${st} ${traceClass}${x.grouped?' is-grouped':''}${changed?' status-changed':''}" data-step="${escapeHtml(key)}" data-status="${label}" aria-label="${escapeHtml(aria)}" style="--step-progress:${percent}%"${title}><div class="step-dot"><span>${timelineIcon(rawStatus)}</span>${groupBadge}</div><div><strong>${escapeHtml(x.label||`Step ${i+1}`)}</strong><small>${escapeHtml(meta)}</small>${reasonLine}</div></div>`;
492
  }).join('');
493
  Object.keys(timelineRenderState).forEach(k=>delete timelineRenderState[k]);
494
  Object.assign(timelineRenderState,nextState);
 
815
  // Legacy test anchors retained for timeline invariants:
816
  // currentStepLabel:activeItem?.label
817
  // x.done_count||0
818
+ // v164 compatibility anchors for older timeline tests:
819
+ // display_label:'Model card'
820
+ // active_substep_label:workerStepDisplayLabel(activeSubstep)
821
+ // title="${escapeHtml(reason)}"
web/static/runs.js CHANGED
@@ -415,38 +415,129 @@ async function selectRun(runId){
415
  const report=$('reportPreview');if(report)report.textContent='Could not load run details: '+e.message;
416
  }
417
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  async function deleteRunById(runId,options={}){
419
- const detail=options.detail||state.selectedRunDetail||state.validationDetail||{};
420
- const summary=detail.summary||{};
421
- runId=runId||summary.run_id||detail.run_id||state.runId;
422
- if(!runId){showMessage('Select a run before deleting it.','warning');return}
423
- const status=String(summary.status||detail.status||detail.state?.status||'').toLowerCase();
424
- if(runMatchesStatus({status},'running')&&!confirm(`Run ${runId} looks active. Delete its bucket folder anyway?`))return;
425
- if(!confirm(`Delete run ${runId} from the bucket? This removes its artifacts, logs, state, and report.`))return;
426
- const buttonId=options.buttonId===null?null:(options.buttonId||(options.kind==='validation'?'deleteValidationRun':'deleteActiveRun'));
 
 
 
427
  setButtonBusy(buttonId,true,'Deleting…');
428
  try{
429
  await apiDelete(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}`);
430
  showMessage(`Deleted run ${runId}`,'success');
431
- if(state.runId===runId){
432
- if(state.poll){clearInterval(state.poll);state.poll=null}
433
- state.runId=null;
434
- state.activeRunProgress=null;
435
- state.selectedRunDetail=null;
436
- state.activeLinks={};
437
- clearSavedRun();
438
- resetRunDetails();
439
- }
440
- if(state.validationRunId===runId){
441
- stopValidationPolling();
442
- state.validationRunId=null;
443
- state.validationDetail=null;
444
- renderValidationStatus({status:'pending',run_id:'—',events:[]});
445
- setText('spaceTestOutput','—');
446
- setText('spaceTestLatency','—');
447
- setText('spaceTestVerdict','pending');
448
- const btn=$('deleteValidationRun');if(btn)btn.disabled=true;
449
- }
450
  if(state.runDetailCache)delete state.runDetailCache[runId];
451
  state.runsCache=(state.runsCache||[]).filter(r=>r.run_id!==runId);
452
  document.querySelectorAll(`.run-row[data-run="${CSS.escape(runId)}"]`).forEach(row=>row.classList.remove('selected'));
@@ -455,8 +546,13 @@ async function deleteRunById(runId,options={}){
455
  }catch(e){
456
  showMessage(e.message,true);
457
  }finally{
 
458
  setButtonBusy(buttonId,false);
459
- const btn=$(buttonId);if(btn)btn.disabled=true;
 
 
 
 
460
  }
461
  }
462
  async function deleteSelectedRun(){return deleteRunById(null,{kind:'active',detail:state.selectedRunDetail||state.activeRunProgress||{}})}
@@ -468,14 +564,24 @@ function defaultRunDocuments(detail={}){
468
  const blob=rel=>runId&&bucket?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}/${rel}`:'';
469
  const tree=rel=>rootUrl?`${rootUrl}/${rel}`:'';
470
  return [
471
- {id:'pi_raw_trace',label:'Pi RAW',subtitle:'Unified build/diagnosis/repair trace',icon:'🧾',present:false,url:blob('traces/raw/agent_trace.jsonl')||tree('traces/raw'),sensitivity:'raw'},
472
- {id:'pi_redacted_trace',label:'Pi redacted',subtitle:'Unified safe agent trace',icon:'🛡️',present:false,url:blob('traces/redacted/agent_trace.jsonl')||tree('traces/redacted'),sensitivity:'safe'},
473
- {id:'report',label:'Report',subtitle:'Human summary',icon:'📄',present:false,url:blob('report.md')},
474
- {id:'smoke',label:'Smoke',subtitle:'Validation + latency',icon:'⚡',present:false,url:blob('tests/generation_smoke.json')},
475
- {id:'repair_decision',label:'Decision',subtitle:'Pi diagnosis action',icon:'◇',present:false,url:blob('repair/REPAIR_DECISION.json')},
476
- {id:'blockage',label:'Blockage',subtitle:'Terminal recovery reason',icon:'!',tone:'warn',present:false,url:blob('repair/BLOCKAGE.json')},
477
  ];
478
  }
 
 
 
 
 
 
 
 
 
 
479
  function deriveRunDocuments(detail={}){
480
  const docs=Array.isArray(detail.run_documents)?detail.run_documents:defaultRunDocuments(detail);
481
  const files=Array.isArray(detail.files)?detail.files:[];
@@ -495,7 +601,7 @@ function deriveRunDocuments(detail={}){
495
  const folderUrl=runId&&bucket?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}/${prefix.replace(/\/$/,'')}`:'';
496
  const existingUrl=String(doc.url||'');
497
  const existingIsFolder=existingUrl.endsWith(`/${prefix.replace(/\/$/,'')}`);
498
- return {present:Boolean(hasContent||doc.present&&existingIsFolder),url:folderUrl||(existingIsFolder?existingUrl:'')};
499
  };
500
  const rawTrace=traceDocState('traces/raw',byId.get('pi_raw_trace'));
501
  byId.get('pi_raw_trace').present=rawTrace.present;
@@ -508,7 +614,7 @@ function deriveRunDocuments(detail={}){
508
  if(shouldShowBlockers&&!byId.has('blockers')){
509
  const runId=summary.run_id||detail.run_id||state.runId;
510
  const bucket=summary.bucket_source||detail.bucket_source||state.bucketSource;
511
- byId.set('blockers',{id:'blockers',label:'Blockers',subtitle:'Why inference is blocked',icon:'⚠️',tone:'warn',present:Boolean(Object.keys(blockers).length||filePaths.has('generated/TECHNICAL_BLOCKERS.json')),url:runId&&bucket?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}/generated/TECHNICAL_BLOCKERS.json`:''});
512
  }
513
  if(byId.has('repair_decision')){
514
  byId.get('repair_decision').present=Boolean((byId.get('repair_decision').present&&byId.get('repair_decision').url)||nonEmptyObject(detail.repair_decision)||filePaths.has('repair/REPAIR_DECISION.json'));
@@ -519,27 +625,37 @@ function deriveRunDocuments(detail={}){
519
  if(!byId.has('repair')&&hasPrefix('repair')){
520
  const runId=summary.run_id||detail.run_id||state.runId;
521
  const bucket=summary.bucket_source||detail.bucket_source||state.bucketSource;
522
- byId.set('repair',{id:'repair',label:'Repair',subtitle:'Brief, plan and summary',icon:'✦',tone:status.includes('failed')?'warn':'neutral',present:true,url:runId&&bucket?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}/repair`:''});
523
  }
524
  const order=['pi_raw_trace','pi_redacted_trace','repair_decision','repair','blockage','report','smoke','blockers'];
525
- return order.map(id=>byId.get(id)).filter(Boolean).filter(d=>{
 
526
  if(['repair_decision','blockage'].includes(d.id))return Boolean(d.present);
527
  if(d.id==='blockers')return shouldShowBlockers||d.present;
528
  return true;
529
  });
 
530
  }
531
  function renderRunDocuments(detail={}){
532
  const root=$('runDocumentsDock');
533
  if(!root)return;
534
- const docs=deriveRunDocuments(detail);
535
- if(!docs.length){root.innerHTML='<div class="empty">Run documents will appear as files are written to the bucket.</div>';return;}
 
 
 
 
 
 
 
 
536
  root.innerHTML=docs.map(doc=>{
537
  const present=Boolean(doc.present&&doc.url);
538
  const tag=present?'a':'span';
539
  const attrs=present?`href="${escapeHtml(doc.url)}" target="_blank" rel="noreferrer"`:'';
540
- const title=present?`Open ${doc.label}`:`${doc.label} is not in the bucket yet`;
541
  const cls=['run-doc-btn',present?'ready':'pending',doc.sensitivity==='raw'?'raw':'',doc.sensitivity==='safe'?'safe':'',doc.tone==='warn'?'warn':''].filter(Boolean).join(' ');
542
- return `<${tag} class="${cls}" ${attrs} title="${escapeHtml(title)}" aria-disabled="${present?'false':'true'}"><span class="doc-icon">${escapeHtml(doc.icon||'📄')}</span><span><strong>${escapeHtml(doc.label)}</strong><em>${escapeHtml(present?doc.subtitle||'Open document':'Pending')}</em></span></${tag}>`;
543
  }).join('');
544
  }
545
 
@@ -592,6 +708,29 @@ function latestRecoveryEvent(detail={},steps=[]){
592
  }
593
  return null;
594
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  function renderAgentRecovery(detail={}){
596
  const card=$('agentRecoveryCard');
597
  const body=$('agentRecoveryBody');
@@ -618,8 +757,10 @@ function renderAgentRecovery(detail={}){
618
  const reason=decision.reason||blockage.reason||blockage.message||diagnosisEvent?.message||'Pi diagnosis is coordinating the next recovery action from collected logs, status and validation evidence.';
619
  const evidence=Array.isArray(decision.evidence)?decision.evidence:[];
620
  const next=decision.next_step||blockage.suggested_next_step||blockage.next_step||'';
 
 
621
  const rows=[];
622
- rows.push(`<div class="recovery-summary"><strong>${escapeHtml(normalizedAction)}</strong><p>${escapeHtml(shortText(reason,220))}</p></div>`);
623
  rows.push(`<div class="recovery-ladder" aria-label="Agent recovery phases">
624
  <span class="${diagnosisEvent||decision.action?'done':'pending'}"><b>1</b><em>Diagnose</em></span>
625
  <span class="${decision.action?'done':'pending'}"><b>2</b><em>Decide</em></span>
@@ -633,6 +774,7 @@ function renderAgentRecovery(detail={}){
633
  if(meta.length)rows.push(`<div class="recovery-meta">${meta.map(([k,v])=>`<div><span>${escapeHtml(k)}</span><strong>${escapeHtml(String(v))}</strong></div>`).join('')}</div>`);
634
  if(evidence.length)rows.push(`<ul class="recovery-evidence">${evidence.slice(0,3).map(x=>`<li>${escapeHtml(shortText(x,120))}</li>`).join('')}</ul>`);
635
  if(next)rows.push(`<p class="recovery-next"><strong>Next:</strong> ${escapeHtml(shortText(next,180))}</p>`);
 
636
  body.innerHTML=rows.join('');
637
  }
638
 
@@ -651,9 +793,9 @@ function renderActiveRunActions(summary={},detail={}){
651
  setLink('activeOpenArtifacts',artifacts);
652
  const prefill=$('activePrefillSpaceTest');
653
  if(prefill){
654
- const canPrefill=Boolean(targetSpace&&runHasValidationResult(detail,summary));
655
  prefill.disabled=!canPrefill;
656
- prefill.title=canPrefill?'Prefill Space Test from this validated build':'Space Test prefill becomes available after validation produced a result.';
657
  prefill.onclick=canPrefill?prepareValidationFromActiveRun:null;
658
  }
659
  const del=$('deleteActiveRun');
@@ -665,7 +807,7 @@ function renderActiveRunActions(summary={},detail={}){
665
  }
666
  }
667
  function renderSelectedLinks(summary={},detail={}){renderActiveRunActions(summary,detail)}
668
- function renderBlockers(detail={}){const root=$('selectedBlockers');if(!root)return;const detailRunId=detail.run_id||detail.summary?.run_id||detail.state?.run_id||'';if(detailRunId&&state.runId&&detailRunId!==state.runId){root.hidden=true;root.innerHTML='';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:[];const piResolution=detail.pi_model_resolution||detail.state?.pi_model_resolution||summary.pi_model_resolution||{};const piMismatch=Boolean(piResolution.mismatch);if(!manual&&!blockerItems.length&&!piMismatch){root.hidden=true;root.innerHTML='';return}root.hidden=false;const parts=[];if(piMismatch)parts.push(`<strong>Pi assistant model changed</strong><p>Requested ${escapeHtml(piResolution.requested_model||'—')}, but Pi/provider traces indicate ${escapeHtml(piResolution.effective_model||'another model')}. This may be an Inference Providers routing/fallback behavior.</p>`);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('')}
669
  function renderRunDetail(detail){
670
  state.selectedRunDetail=detail;
671
  const summary=detail.summary||{};
 
415
  const report=$('reportPreview');if(report)report.textContent='Could not load run details: '+e.message;
416
  }
417
  }
418
+ function runDeletionContext(runId,options={}){
419
+ const kind=options.kind==='validation'?'validation':'active';
420
+ const explicit=options.detail||{};
421
+ const candidate=kind==='validation'?(explicit.run_id||explicit.summary?.run_id?explicit:state.validationDetail||{}):(explicit.run_id||explicit.summary?.run_id?explicit:state.selectedRunDetail||state.activeRunProgress||{});
422
+ const summary=candidate.summary||{};
423
+ const resolvedRunId=runId||summary.run_id||candidate.run_id||(kind==='validation'?state.validationRunId:state.runId);
424
+ return {kind,runId:resolvedRunId,detail:candidate,summary};
425
+ }
426
+ function clearDeletedRunState(runId,kind){
427
+ if(!runId)return;
428
+ if(state.runId===runId){
429
+ if(state.poll){clearInterval(state.poll);state.poll=null}
430
+ state.runId=null;
431
+ state.activeRunProgress=null;
432
+ state.selectedRunDetail=null;
433
+ state.activeLinks={};
434
+ clearSavedRun();
435
+ resetRunDetails();
436
+ }
437
+ if(state.validationRunId===runId||kind==='validation'){
438
+ if(state.validationRunId===runId)stopValidationPolling();
439
+ if(state.validationDetail?.run_id===runId||state.validationDetail?.summary?.run_id===runId||kind==='validation')state.validationDetail=null;
440
+ if(state.validationRunId===runId)state.validationRunId=null;
441
+ renderValidationStatus({status:'pending',run_id:'—',events:[]});
442
+ setText('spaceTestOutput','—');
443
+ setText('spaceTestLatency','—');
444
+ setText('spaceTestVerdict','pending');
445
+ const btn=$('deleteValidationRun');if(btn)btn.disabled=true;
446
+ }
447
+ if(state.selectedRunDetail?.run_id===runId||state.selectedRunDetail?.summary?.run_id===runId)state.selectedRunDetail=null;
448
+ }
449
+ function ensureDeleteRunDialog(){
450
+ let modal=$('deleteRunDialog');
451
+ if(modal)return modal;
452
+ modal=document.createElement('div');
453
+ modal.id='deleteRunDialog';
454
+ modal.className='delete-run-modal';
455
+ modal.hidden=true;
456
+ modal.innerHTML=`<div class="delete-run-backdrop" data-delete-cancel></div>
457
+ <section class="delete-run-card" role="dialog" aria-modal="true" aria-labelledby="deleteRunTitle" aria-describedby="deleteRunBody">
458
+ <button class="delete-run-close" type="button" data-delete-cancel aria-label="Close">×</button>
459
+ <div class="delete-run-icon" aria-hidden="true">⌫</div>
460
+ <div class="delete-run-copy">
461
+ <span class="delete-run-kicker" id="deleteRunKicker">Delete run</span>
462
+ <h3 id="deleteRunTitle">Delete this run?</h3>
463
+ <p id="deleteRunBody">This removes the run folder from your Hugging Face Storage Bucket.</p>
464
+ </div>
465
+ <div class="delete-run-meta" id="deleteRunMeta"></div>
466
+ <div class="delete-run-warning" id="deleteRunWarning" hidden></div>
467
+ <div class="delete-run-actions">
468
+ <button class="secondary" type="button" data-delete-cancel>Keep run</button>
469
+ <button class="primary danger" type="button" data-delete-confirm>Delete run</button>
470
+ </div>
471
+ </section>`;
472
+ document.body.appendChild(modal);
473
+ return modal;
474
+ }
475
+ function confirmRunDeletion({runId,kind='active',status='',detail={}}={}){
476
+ if(state.deleteModalPromise&&state.deleteModalRunId===runId)return state.deleteModalPromise;
477
+ if(state.deleteModalPromise)return state.deleteModalPromise;
478
+ state.deleteModalRunId=runId;
479
+ state.deleteModalPromise=new Promise(resolve=>{
480
+ const modal=ensureDeleteRunDialog();
481
+ const card=modal.querySelector('.delete-run-card');
482
+ const confirmBtn=modal.querySelector('[data-delete-confirm]');
483
+ const cancelEls=modal.querySelectorAll('[data-delete-cancel]');
484
+ const title=$('deleteRunTitle');
485
+ const body=$('deleteRunBody');
486
+ const kicker=$('deleteRunKicker');
487
+ const meta=$('deleteRunMeta');
488
+ const warning=$('deleteRunWarning');
489
+ const normalized=String(status||detail.status||detail.summary?.status||detail.state?.status||'unknown').toLowerCase();
490
+ const active=runMatchesStatus({status:normalized},'running');
491
+ const label=kind==='validation'?'validation run':'build run';
492
+ kicker.textContent=kind==='validation'?'Space Test deletion':'Build run deletion';
493
+ title.textContent=`Delete this ${label}?`;
494
+ body.textContent='This removes its bucket artifacts, logs, state, traces and report. The generated Space or tested Space is not deleted.';
495
+ meta.innerHTML=`<span><strong>Run</strong><code>${escapeHtml(runId||'—')}</code></span><span><strong>Type</strong>${escapeHtml(kind==='validation'?'Space Test':'Build')}</span><span><strong>Status</strong>${escapeHtml(badgeLabel(normalized))}</span>`;
496
+ warning.hidden=!active;
497
+ warning.textContent=active?'This run still looks active. Deleting only removes stored run files; it does not cancel a running Job.':'Deletion is permanent for this run folder.';
498
+ confirmBtn.textContent=active?'Delete stored files':'Delete run';
499
+ const previousFocus=document.activeElement;
500
+ let done=false;
501
+ const close=value=>{
502
+ if(done)return;done=true;
503
+ modal.hidden=true;
504
+ modal.classList.remove('is-open');
505
+ document.removeEventListener('keydown',onKey);
506
+ cancelEls.forEach(el=>el.removeEventListener('click',onCancel));
507
+ confirmBtn.removeEventListener('click',onConfirm);
508
+ try{previousFocus&&previousFocus.focus&&previousFocus.focus()}catch(_){}
509
+ state.deleteModalPromise=null;
510
+ state.deleteModalRunId=null;
511
+ resolve(value);
512
+ };
513
+ const onCancel=()=>close(false);
514
+ const onConfirm=()=>close(true);
515
+ const onKey=e=>{if(e.key==='Escape')close(false);if(e.key==='Enter'&&modal.classList.contains('is-open'))close(true)};
516
+ cancelEls.forEach(el=>el.addEventListener('click',onCancel));
517
+ confirmBtn.addEventListener('click',onConfirm);
518
+ document.addEventListener('keydown',onKey);
519
+ modal.hidden=false;
520
+ requestAnimationFrame(()=>{modal.classList.add('is-open');confirmBtn.focus();});
521
+ });
522
+ return state.deleteModalPromise;
523
+ }
524
  async function deleteRunById(runId,options={}){
525
+ const context=runDeletionContext(runId,options);
526
+ runId=context.runId;
527
+ if(!runId){showMessage(context.kind==='validation'?'Select a validation run before deleting it.':'Select a run before deleting it.','warning');return}
528
+ if(state.deletingRuns&&state.deletingRuns[runId])return;
529
+ const status=String(context.summary.status||context.detail.status||context.detail.state?.status||'').toLowerCase();
530
+ const confirmed=await confirmRunDeletion({runId,kind:context.kind,status,detail:context.detail});
531
+ if(!confirmed)return;
532
+ if(state.deletingRuns&&state.deletingRuns[runId])return;
533
+ state.deletingRuns=state.deletingRuns||{};
534
+ state.deletingRuns[runId]=true;
535
+ const buttonId=options.buttonId===null?null:(options.buttonId||(context.kind==='validation'?'deleteValidationRun':'deleteActiveRun'));
536
  setButtonBusy(buttonId,true,'Deleting…');
537
  try{
538
  await apiDelete(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}`);
539
  showMessage(`Deleted run ${runId}`,'success');
540
+ clearDeletedRunState(runId,context.kind);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  if(state.runDetailCache)delete state.runDetailCache[runId];
542
  state.runsCache=(state.runsCache||[]).filter(r=>r.run_id!==runId);
543
  document.querySelectorAll(`.run-row[data-run="${CSS.escape(runId)}"]`).forEach(row=>row.classList.remove('selected'));
 
546
  }catch(e){
547
  showMessage(e.message,true);
548
  }finally{
549
+ if(state.deletingRuns)delete state.deletingRuns[runId];
550
  setButtonBusy(buttonId,false);
551
+ if(buttonId){
552
+ const btn=$(buttonId);
553
+ const currentId=buttonId==='deleteActiveRun'?state.runId:state.validationRunId;
554
+ if(btn)btn.disabled=!(currentId&&currentId===runId);
555
+ }
556
  }
557
  }
558
  async function deleteSelectedRun(){return deleteRunById(null,{kind:'active',detail:state.selectedRunDetail||state.activeRunProgress||{}})}
 
564
  const blob=rel=>runId&&bucket?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}/${rel}`:'';
565
  const tree=rel=>rootUrl?`${rootUrl}/${rel}`:'';
566
  return [
567
+ {id:'pi_raw_trace',label:'Pi RAW',subtitle:'Source folder',icon:'🧾',present:false,url:blob('traces/raw/agent_trace.jsonl')||tree('traces/raw'),sensitivity:'raw'},
568
+ {id:'pi_redacted_trace',label:'Pi redacted',subtitle:'Source folder',icon:'🛡️',present:false,url:blob('traces/redacted/agent_trace.jsonl')||tree('traces/redacted'),sensitivity:'safe'},
569
+ {id:'report',label:'Report',subtitle:'Source document',icon:'📄',present:false,url:blob('report.md')},
570
+ {id:'smoke',label:'Smoke',subtitle:'Source JSON',icon:'⚡',present:false,url:blob('tests/generation_smoke.json')},
571
+ {id:'repair_decision',label:'Decision',subtitle:'Source JSON',icon:'◇',present:false,url:blob('repair/REPAIR_DECISION.json')},
572
+ {id:'blockage',label:'Blockage',subtitle:'Source JSON',icon:'!',tone:'warn',present:false,url:blob('repair/BLOCKAGE.json')},
573
  ];
574
  }
575
+ function mergeStableRunDocuments(runId,docs=[]){
576
+ state.documentCache=state.documentCache||{};
577
+ const cached=Array.isArray(state.documentCache[runId])?state.documentCache[runId]:[];
578
+ const byId=new Map(cached.map(d=>[d.id,{...d}]));
579
+ for(const doc of docs||[]){
580
+ const previous=byId.get(doc.id)||{};
581
+ byId.set(doc.id,{...previous,...doc,present:Boolean(doc.present||previous.present),url:doc.url||previous.url||''});
582
+ }
583
+ return docs.map(doc=>byId.get(doc.id)||doc);
584
+ }
585
  function deriveRunDocuments(detail={}){
586
  const docs=Array.isArray(detail.run_documents)?detail.run_documents:defaultRunDocuments(detail);
587
  const files=Array.isArray(detail.files)?detail.files:[];
 
601
  const folderUrl=runId&&bucket?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}/${prefix.replace(/\/$/,'')}`:'';
602
  const existingUrl=String(doc.url||'');
603
  const existingIsFolder=existingUrl.endsWith(`/${prefix.replace(/\/$/,'')}`);
604
+ return {present:Boolean(hasContent||doc.present),url:folderUrl||existingUrl};
605
  };
606
  const rawTrace=traceDocState('traces/raw',byId.get('pi_raw_trace'));
607
  byId.get('pi_raw_trace').present=rawTrace.present;
 
614
  if(shouldShowBlockers&&!byId.has('blockers')){
615
  const runId=summary.run_id||detail.run_id||state.runId;
616
  const bucket=summary.bucket_source||detail.bucket_source||state.bucketSource;
617
+ byId.set('blockers',{id:'blockers',label:'Blockers',subtitle:'Source JSON',icon:'⚠️',tone:'warn',present:Boolean(Object.keys(blockers).length||filePaths.has('generated/TECHNICAL_BLOCKERS.json')),url:runId&&bucket?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}/generated/TECHNICAL_BLOCKERS.json`:''});
618
  }
619
  if(byId.has('repair_decision')){
620
  byId.get('repair_decision').present=Boolean((byId.get('repair_decision').present&&byId.get('repair_decision').url)||nonEmptyObject(detail.repair_decision)||filePaths.has('repair/REPAIR_DECISION.json'));
 
625
  if(!byId.has('repair')&&hasPrefix('repair')){
626
  const runId=summary.run_id||detail.run_id||state.runId;
627
  const bucket=summary.bucket_source||detail.bucket_source||state.bucketSource;
628
+ byId.set('repair',{id:'repair',label:'Repair',subtitle:'Source folder',icon:'✦',tone:status.includes('failed')?'warn':'neutral',present:true,url:runId&&bucket?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}/repair`:''});
629
  }
630
  const order=['pi_raw_trace','pi_redacted_trace','repair_decision','repair','blockage','report','smoke','blockers'];
631
+ const runId=String(summary.run_id||detail.run_id||state.runId||'');
632
+ const resolved=order.map(id=>byId.get(id)).filter(Boolean).filter(d=>{
633
  if(['repair_decision','blockage'].includes(d.id))return Boolean(d.present);
634
  if(d.id==='blockers')return shouldShowBlockers||d.present;
635
  return true;
636
  });
637
+ return runId?mergeStableRunDocuments(runId,resolved):resolved;
638
  }
639
  function renderRunDocuments(detail={}){
640
  const root=$('runDocumentsDock');
641
  if(!root)return;
642
+ const runId=String(detail.run_id||detail.summary?.run_id||state.runId||'');
643
+ state.documentCache=state.documentCache||{};
644
+ let docs=deriveRunDocuments(detail);
645
+ const hasReady=docs.some(d=>d.present&&d.url);
646
+ if(hasReady||!state.documentCache[runId])state.documentCache[runId]=mergeStableRunDocuments(runId,docs);
647
+ docs=state.documentCache[runId]||docs;
648
+ if(!docs.length){root.innerHTML='<div class="empty compact-empty loading-shimmer">Run documents are being indexed…</div>';return;}
649
+ const signature=JSON.stringify(docs.map(d=>[d.id,d.present,d.url,d.label,d.subtitle,d.tone]));
650
+ if(root.dataset.signature===signature)return;
651
+ root.dataset.signature=signature;
652
  root.innerHTML=docs.map(doc=>{
653
  const present=Boolean(doc.present&&doc.url);
654
  const tag=present?'a':'span';
655
  const attrs=present?`href="${escapeHtml(doc.url)}" target="_blank" rel="noreferrer"`:'';
656
+ const title=present?`Open source: ${doc.label}`:`${doc.label} is not in the bucket yet`;
657
  const cls=['run-doc-btn',present?'ready':'pending',doc.sensitivity==='raw'?'raw':'',doc.sensitivity==='safe'?'safe':'',doc.tone==='warn'?'warn':''].filter(Boolean).join(' ');
658
+ return `<${tag} class="${cls}" ${attrs} title="${escapeHtml(title)}" aria-disabled="${present?'false':'true'}"><span class="doc-icon" aria-hidden="true">${escapeHtml(doc.icon||'📄')}</span><strong>${escapeHtml(doc.label)}</strong><em>${escapeHtml(present?doc.subtitle||'Open source':'Pending')}</em></${tag}>`;
659
  }).join('');
660
  }
661
 
 
708
  }
709
  return null;
710
  }
711
+
712
+ function recoverySourceDocuments(detail={}){
713
+ const docs=deriveRunDocuments(detail).filter(d=>d&&d.present&&d.url);
714
+ const byId=new Map(docs.map(d=>[d.id,d]));
715
+ const links=[];
716
+ const add=(id,label)=>{const d=byId.get(id);if(d&&d.url)links.push({label,url:d.url,tone:d.tone||'',sensitivity:d.sensitivity||''});};
717
+ add('repair_decision','Decision');
718
+ add('blockage','Blockage');
719
+ add('pi_redacted_trace','Pi trace');
720
+ add('pi_raw_trace','RAW trace');
721
+ return links.slice(0,4);
722
+ }
723
+ function recoveryPhaseLabel(detail={},decision={},blockage={}){
724
+ const summary=detail.summary||{};
725
+ const status=String(summary.status||detail.state?.status||detail.status||'').toLowerCase();
726
+ if(nonEmptyObject(blockage)||status.includes('blocker')||status.includes('manual'))return'Final blocker';
727
+ if(decision.action&&TERMINAL_STATUSES.has(status))return'Final decision';
728
+ if(latestRecoveryEvent(detail,['repair_validation','generation_smoke','inference_gate']))return'Revalidating';
729
+ if(latestRecoveryEvent(detail,['repair_brief','repair_plan','repair_patch','repair_upload']))return'Repairing';
730
+ if(decision.action)return'Decision made';
731
+ return'Live diagnosis';
732
+ }
733
+
734
  function renderAgentRecovery(detail={}){
735
  const card=$('agentRecoveryCard');
736
  const body=$('agentRecoveryBody');
 
757
  const reason=decision.reason||blockage.reason||blockage.message||diagnosisEvent?.message||'Pi diagnosis is coordinating the next recovery action from collected logs, status and validation evidence.';
758
  const evidence=Array.isArray(decision.evidence)?decision.evidence:[];
759
  const next=decision.next_step||blockage.suggested_next_step||blockage.next_step||'';
760
+ const phase=recoveryPhaseLabel(detail,decision,blockage);
761
+ const sourceDocs=recoverySourceDocuments(detail);
762
  const rows=[];
763
+ rows.push(`<div class="recovery-summary"><strong>${escapeHtml(phase)} · ${escapeHtml(normalizedAction)}</strong><p>${escapeHtml(shortText(reason,220))}</p></div>`);
764
  rows.push(`<div class="recovery-ladder" aria-label="Agent recovery phases">
765
  <span class="${diagnosisEvent||decision.action?'done':'pending'}"><b>1</b><em>Diagnose</em></span>
766
  <span class="${decision.action?'done':'pending'}"><b>2</b><em>Decide</em></span>
 
774
  if(meta.length)rows.push(`<div class="recovery-meta">${meta.map(([k,v])=>`<div><span>${escapeHtml(k)}</span><strong>${escapeHtml(String(v))}</strong></div>`).join('')}</div>`);
775
  if(evidence.length)rows.push(`<ul class="recovery-evidence">${evidence.slice(0,3).map(x=>`<li>${escapeHtml(shortText(x,120))}</li>`).join('')}</ul>`);
776
  if(next)rows.push(`<p class="recovery-next"><strong>Next:</strong> ${escapeHtml(shortText(next,180))}</p>`);
777
+ if(sourceDocs.length)rows.push(`<div class="recovery-docs"><span>Sources</span>${sourceDocs.map(d=>`<a href="${escapeHtml(d.url)}" target="_blank" rel="noreferrer" class="${escapeHtml(d.tone||d.sensitivity||'')}">${escapeHtml(d.label)}</a>`).join('')}</div>`);
778
  body.innerHTML=rows.join('');
779
  }
780
 
 
793
  setLink('activeOpenArtifacts',artifacts);
794
  const prefill=$('activePrefillSpaceTest');
795
  if(prefill){
796
+ const canPrefill=Boolean(targetSpace);
797
  prefill.disabled=!canPrefill;
798
+ prefill.title=canPrefill?'Prefill Space Test from this build':'Space Test prefill becomes available as soon as the target Space is known.';
799
  prefill.onclick=canPrefill?prepareValidationFromActiveRun:null;
800
  }
801
  const del=$('deleteActiveRun');
 
807
  }
808
  }
809
  function renderSelectedLinks(summary={},detail={}){renderActiveRunActions(summary,detail)}
810
+ function renderBlockers(detail={}){const root=$('selectedBlockers');if(!root)return;const detailRunId=detail.run_id||detail.summary?.run_id||detail.state?.run_id||'';if(detailRunId&&state.runId&&detailRunId!==state.runId){root.hidden=true;root.innerHTML='';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:[]).filter(b=>!['patch_code','repair_decision','repair','recovery'].includes(String(b.type||b.name||'').toLowerCase()));const piResolution=detail.pi_model_resolution||detail.state?.pi_model_resolution||summary.pi_model_resolution||{};const piMismatch=Boolean(piResolution.mismatch);if(!manual&&!blockerItems.length&&!piMismatch){root.hidden=true;root.innerHTML='';return}root.hidden=false;const parts=[];if(piMismatch)parts.push(`<strong>Pi assistant model changed</strong><p>Requested ${escapeHtml(piResolution.requested_model||'—')}, but Pi/provider traces indicate ${escapeHtml(piResolution.effective_model||'another model')}. This may be an Inference Providers routing/fallback behavior.</p>`);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('')}
811
  function renderRunDetail(detail){
812
  state.selectedRunDetail=detail;
813
  const summary=detail.summary||{};