1 (function () { 2 const DATA_BASE = "data/"; 3 4 const els = { 5 projectDonut: document.getElementById("project-donut"), 6 globalPills: document.getElementById("global-pills"), 7 stageNav: document.getElementById("stage-nav"), 8 heroTitle: document.getElementById("hero-title"), 9 heroSubtitle: document.getElementById("hero-subtitle"), 10 stageOrderBadge: document.getElementById("stage-order-badge"), 11 selectedStageName: document.getElementById("selected-stage-name"), 12 selectedStageDesc: document.getElementById("selected-stage-desc"), 13 selectedStageTotal: document.getElementById("selected-stage-total"), 14 selectedStageDonut: document.getElementById("selected-stage-donut"), 15 selectedRuntimeBadge: document.getElementById("selected-runtime-badge"), 16 selectedPipelineSteps: document.getElementById("selected-pipeline-steps"), 17 hostStatusBadge: document.getElementById("host-status-badge"), 18 dockerStatusBadge: document.getElementById("docker-status-badge"), 19 hostRuntimeGrid: document.getElementById("host-runtime-grid"), 20 dockerRuntimeGrid: document.getElementById("docker-runtime-grid"), 21 pipelineRuntimeCount: document.getElementById("pipeline-runtime-count"), 22 pipelineRuntimeBoard: document.getElementById("pipeline-runtime-board"), 23 drawerLabel: document.getElementById("drawer-label"), 24 jsonViewer: document.getElementById("json-viewer"), 25 btnOpenManifest: document.getElementById("btn-open-manifest"), 26 btnOpenContract: document.getElementById("btn-open-contract"), 27 btnOpenHost: document.getElementById("btn-open-host"), 28 btnOpenDocker: document.getElementById("btn-open-docker"), 29 btnOpenRuntime: document.getElementById("btn-open-runtime"), 30 dataViewerModal: document.getElementById("data-viewer-modal") 31 }; 32 33 const state = { 34 manifest: null, 35 hubIndex: null, 36 contentIndex: null, 37 navigationSpec: null, 38 pipelines: null, 39 hostRuntime: null, 40 dockerRuntime: null, 41 runtimeStatus: null, 42 projectProgress: null, 43 selectedStage: null 44 }; 45 46 47 const BUCKETS = [ 48 "authority_docs" 49 ]; 50 51 const BUCKET_LABELS = { 52 authority_docs: "Authority Documents" 53 }; 54 55 state.openPhases = new Set(); 56 state.openCategories = new Set(); 57 state.selectedCategory = null; 58 state.selectedBucket = null; 59 60 function phaseOfCategory(cat) { 61 const explicit = normStageKey(cat && cat.phase_id); 62 if (explicit && /^phase_\d{2}$/.test(explicit)) { 63 return explicit; 64 } 65 66 const docs = Array.isArray(cat && cat.docs) ? cat.docs : []; 67 for (const doc of docs) { 68 const ph = normStageKey(doc && doc.phase); 69 if (/^phase_\d{2}$/.test(ph)) return ph; 70 } 71 72 const badge = String(cat && cat.badge || ""); 73 const mm = badge.match(/\d+/); 74 if (!mm) return normStageKey("phase-01"); 75 return normStageKey("phase-" + String(parseInt(mm[0], 10)).padStart(2, "0")); 76 } 77 78 function categoriesForPhase(phaseId) { 79 const cats = (state.hubIndex && Array.isArray(state.hubIndex.categories)) ? state.hubIndex.categories : []; 80 const key = normStageKey(phaseId); 81 const seen = new Set(); 82 83 return cats.filter(function (cat) { 84 const docs = Array.isArray(cat.docs) ? cat.docs : []; 85 const docPhaseHit = docs.some(function (doc) { 86 return normStageKey(doc && doc.phase) === key; 87 }); 88 89 const badgeHit = phaseOfCategory(cat) === key; 90 const match = docPhaseHit || badgeHit; 91 92 if (!match) return false; 93 94 const catId = String(cat && cat.id || ""); 95 if (seen.has(catId)) return false; 96 seen.add(catId); 97 return true; 98 }); 99 } 100 101 function phaseStatusClass(phaseId) { 102 const roll = stageRollup(phaseId); 103 return getBadgeClass(roll ? runtimeStatusOf(roll) : "MISSING"); 104 } 105 106 function renderCanonicalNav() { 107 if (!els.stageNav) return; 108 109 const phases = (state.hubIndex && Array.isArray(state.hubIndex.phases)) ? state.hubIndex.phases : []; 110 if (!phases.length) { 111 els.stageNav.innerHTML = '
MISSING hub phases / hubIndex binding
'; 112 return; 113 } 114 115 function sameLabel(a, b) { 116 return String(a || "").trim().toLowerCase() === String(b || "").trim().toLowerCase(); 117 } 118 119 function renderBuckets(phaseId, catId) { 120 return [ 121 '
', 122 BUCKETS.map(function (bucket) { 123 return [ 124 '
', 125 '', 128 '
' 129 ].join(""); 130 }).join(""), 131 '
' 132 ].join(""); 133 } 134 135 const html = phases.map(function (phase) { 136 const phaseId = normStageKey(phase.id || phase.stage_key || ""); 137 const isOpen = state.openPhases.has(phaseId); 138 const cats = categoriesForPhase(phaseId); 139 const phaseBadgeClass = phaseStatusClass(phaseId); 140 const phaseNum = String(phase.step || "").replace("Phase ", ""); 141 142 return [ 143 '
', 144 '', 150 (isOpen ? [ 151 '
', 152 cats.map(function (cat) { 153 const catId = String(cat.id || ""); 154 const catOpen = state.openCategories.has(catId); 155 const collapseCategory = cats.length === 1 && sameLabel(cat.title, phase.name); 156 157 if (collapseCategory) { 158 return [ 159 '
', 160 renderBuckets(phaseId, catId), 161 '
' 162 ].join(""); 163 } 164 165 return [ 166 '
', 167 '', 171 (catOpen ? renderBuckets(phaseId, catId) : ''), 172 '
' 173 ].join(""); 174 }).join(""), 175 '
' 176 ].join("") : ''), 177 '
' 178 ].join(""); 179 }).join(""); 180 181 els.stageNav.innerHTML = html; 182 bindCanonicalNav(); 183 } 184 185 function bindCanonicalNav() { 186 document.querySelectorAll(".tree-phase-row").forEach(function (btn) { 187 btn.addEventListener("click", function () { 188 const phaseId = normStageKey(btn.dataset.phaseId); 189 const wasOpen = state.openPhases.has(phaseId); 190 191 state.openPhases = wasOpen ? new Set() : new Set([phaseId]); 192 193 if (!wasOpen) { 194 state.openCategories = new Set( 195 Array.from(state.openCategories).filter(function (catId) { 196 const cats = (state.hubIndex && Array.isArray(state.hubIndex.categories)) ? state.hubIndex.categories : []; 197 const cat = cats.find(function (c) { return String(c.id || "") === String(catId); }); 198 return cat && phaseOfCategory(cat) === phaseId; 199 }) 200 ); 201 } else { 202 state.openCategories = new Set(); 203 state.selectedCategory = null; 204 state.selectedBucket = null; 205 } 206 207 state.selectedStage = phaseId; 208 renderCanonicalNav(); 209 selectStage(phaseId); 210 }); 211 }); 212 213 document.querySelectorAll(".tree-cat-row").forEach(function (btn) { 214 btn.addEventListener("click", function (ev) { 215 ev.stopPropagation(); 216 const phaseId = normStageKey(btn.dataset.phaseId); 217 const catId = btn.dataset.catId; 218 219 state.openPhases = new Set([phaseId]); 220 state.openCategories = new Set([catId]); 221 222 state.selectedStage = phaseId; 223 state.selectedCategory = catId; 224 state.selectedBucket = null; 225 226 renderCanonicalNav(); 227 selectStage(phaseId); 228 }); 229 }); 230 231 document.querySelectorAll(".tree-bucket-row").forEach(function (btn) { 232 btn.addEventListener("click", function (ev) { 233 ev.stopPropagation(); 234 const phaseId = normStageKey(btn.dataset.phaseId); 235 const catId = btn.dataset.catId || null; 236 237 state.openPhases = new Set([phaseId]); 238 state.openCategories = catId ? new Set([catId]) : new Set(); 239 240 state.selectedStage = phaseId; 241 state.selectedCategory = catId; 242 state.selectedBucket = btn.dataset.bucket || null; 243 244 renderCanonicalNav(); 245 selectStage(phaseId); 246 }); 247 }); 248 } 249 250 251 function safeUpper(v) { 252 return String(v || "MISSING").trim().toUpperCase(); 253 } 254 255 function getBadgeClass(status) { 256 const s = safeUpper(status); 257 if (s === "PASS" || s === "OK" || s === "SUCCESS") return "pass"; 258 if (s === "FAIL" || s === "ERROR") return "fail"; 259 if (s === "RUNNING") return "running"; 260 if (s === "PENDING") return "pending";