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";