===== function renderCanonicalNav( ===== 00099: }); 00100: } 00101: 00102: function phaseStatusClass(phaseId) { 00103: const roll = stageRollup(phaseId); 00104: return getBadgeClass(roll ? runtimeStatusOf(roll) : "MISSING"); 00105: } 00106: 00107: function renderCanonicalNav() { 00108: if (!els.stageNav) return; 00109: 00110: const phases = (state.hubIndex && Array.isArray(state.hubIndex.phases)) ? state.hubIndex.phases : []; 00111: if (!phases.length) { 00112: els.stageNav.innerHTML = '
MISSING hub phases / hubIndex binding
'; 00113: return; 00114: } 00115: 00116: function sameLabel(a, b) { 00117: return String(a || "").trim().toLowerCase() === String(b || "").trim().toLowerCase(); 00118: } 00119: 00120: function renderBuckets(phaseId, catId) { 00121: return [ 00122: '
', 00123: BUCKETS.map(function (bucket) { 00124: return [ 00125: '
', 00126: '', 00129: '
' 00130: ].join(""); 00131: }).join(""), 00132: '
' 00133: ].join(""); 00134: } 00135: 00136: const html = phases.map(function (phase) { 00137: const phaseId = normStageKey(phase.id || phase.stage_key || ""); 00138: const isOpen = state.openPhases.has(phaseId); 00139: const cats = categoriesForPhase(phaseId); 00140: const phaseBadgeClass = phaseStatusClass(phaseId); 00141: const phaseNum = String(phase.step || "").replace("Phase ", ""); 00142: 00143: return [ 00144: '
', 00145: '', 00151: (isOpen ? [ 00152: '
', 00153: cats.map(function (cat) { 00154: const catId = String(cat.id || ""); 00155: const catOpen = state.openCategories.has(catId); 00156: const collapseCategory = cats.length === 1 && sameLabel(cat.title, phase.name); 00157: 00158: if (collapseCategory) { 00159: return [ 00160: '
', 00161: renderBuckets(phaseId, catId), 00162: '
' 00163: ].join(""); 00164: } 00165: 00166: return [ 00167: '
', 00168: '', 00172: (catOpen ? renderBuckets(phaseId, catId) : ''), 00173: '
' 00174: ].join(""); 00175: }).join(""), 00176: '
' 00177: ].join("") : ''), 00178: '
' 00179: ].join(""); 00180: }).join(""); 00181: 00182: els.stageNav.innerHTML = html; 00183: bindCanonicalNav(); 00184: } 00185: 00186: function bindCanonicalNav() { 00187: document.querySelectorAll(".tree-phase-row").forEach(function (btn) { 00188: btn.addEventListener("click", function () { 00189: const phaseId = normStageKey(btn.dataset.phaseId); 00190: const wasOpen = state.openPhases.has(phaseId); 00191: 00192: state.openPhases = wasOpen ? new Set() : new Set([phaseId]); 00193: 00194: if (!wasOpen) { 00195: state.openCategories = new Set( 00196: Array.from(state.openCategories).filter(function (catId) { 00197: const cats = (state.hubIndex && Array.isArray(state.hubIndex.categories)) ? state.hubIndex.categories : []; 00198: const cat = cats.find(function (c) { return String(c.id || "") === String(catId); }); 00199: return cat && phaseOfCategory(cat) === phaseId; 00200: }) 00201: ); 00202: } else { 00203: state.openCategories = new Set(); 00204: state.selectedCategory = null; 00205: state.selectedBucket = null; 00206: } 00207: 00208: state.selectedStage = phaseId; 00209: renderCanonicalNav(); 00210: selectStage(phaseId); 00211: }); 00212: }); 00213: 00214: document.querySelectorAll(".tree-cat-row").forEach(function (btn) { 00215: btn.addEventListener("click", function (ev) { 00216: ev.stopPropagation(); 00217: const phaseId = normStageKey(btn.dataset.phaseId); 00218: const catId = btn.dataset.catId; 00219: 00220: state.openPhases = new Set([phaseId]); 00221: state.openCategories = new Set([catId]); 00222: 00223: state.selectedStage = phaseId; 00224: state.selectedCategory = catId; 00225: state.selectedBucket = null; 00226: 00227: renderCanonicalNav(); 00228: selectStage(phaseId); 00229: }); 00230: }); 00231: 00232: document.querySelectorAll(".tree-bucket-row").forEach(function (btn) { 00233: btn.addEventListener("click", function (ev) { 00234: ev.stopPropagation(); 00235: const phaseId = normStageKey(btn.dataset.phaseId); 00236: const catId = btn.dataset.catId || null; 00237: 00238: state.openPhases = new Set([phaseId]); 00239: state.openCategories = catId ? new Set([catId]) : new Set(); 00240: 00241: state.selectedStage = phaseId; 00242: state.selectedCategory = catId; 00243: state.selectedBucket = btn.dataset.bucket || null; 00244: 00245: renderCanonicalNav(); 00246: selectStage(phaseId); ===== function bindCanonicalNav( ===== 00178: '' 00179: ].join(""); 00180: }).join(""); 00181: 00182: els.stageNav.innerHTML = html; 00183: bindCanonicalNav(); 00184: } 00185: 00186: function bindCanonicalNav() { 00187: document.querySelectorAll(".tree-phase-row").forEach(function (btn) { 00188: btn.addEventListener("click", function () { 00189: const phaseId = normStageKey(btn.dataset.phaseId); 00190: const wasOpen = state.openPhases.has(phaseId); 00191: 00192: state.openPhases = wasOpen ? new Set() : new Set([phaseId]); 00193: 00194: if (!wasOpen) { 00195: state.openCategories = new Set( 00196: Array.from(state.openCategories).filter(function (catId) { 00197: const cats = (state.hubIndex && Array.isArray(state.hubIndex.categories)) ? state.hubIndex.categories : []; 00198: const cat = cats.find(function (c) { return String(c.id || "") === String(catId); }); 00199: return cat && phaseOfCategory(cat) === phaseId; 00200: }) 00201: ); 00202: } else { 00203: state.openCategories = new Set(); 00204: state.selectedCategory = null; 00205: state.selectedBucket = null; 00206: } 00207: 00208: state.selectedStage = phaseId; 00209: renderCanonicalNav(); 00210: selectStage(phaseId); 00211: }); 00212: }); 00213: 00214: document.querySelectorAll(".tree-cat-row").forEach(function (btn) { 00215: btn.addEventListener("click", function (ev) { 00216: ev.stopPropagation(); 00217: const phaseId = normStageKey(btn.dataset.phaseId); 00218: const catId = btn.dataset.catId; 00219: 00220: state.openPhases = new Set([phaseId]); 00221: state.openCategories = new Set([catId]); 00222: 00223: state.selectedStage = phaseId; 00224: state.selectedCategory = catId; 00225: state.selectedBucket = null; 00226: 00227: renderCanonicalNav(); 00228: selectStage(phaseId); 00229: }); 00230: }); 00231: 00232: document.querySelectorAll(".tree-bucket-row").forEach(function (btn) { 00233: btn.addEventListener("click", function (ev) { 00234: ev.stopPropagation(); 00235: const phaseId = normStageKey(btn.dataset.phaseId); 00236: const catId = btn.dataset.catId || null; 00237: 00238: state.openPhases = new Set([phaseId]); 00239: state.openCategories = catId ? new Set([catId]) : new Set(); 00240: 00241: state.selectedStage = phaseId; 00242: state.selectedCategory = catId; 00243: state.selectedBucket = btn.dataset.bucket || null; 00244: 00245: renderCanonicalNav(); 00246: selectStage(phaseId); 00247: }); 00248: }); 00249: } 00250: 00251: 00252: function safeUpper(v) { 00253: return String(v || "MISSING").trim().toUpperCase(); 00254: } 00255: 00256: function getBadgeClass(status) { 00257: const s = safeUpper(status); 00258: if (s === "PASS" || s === "OK" || s === "SUCCESS") return "pass"; 00259: if (s === "FAIL" || s === "ERROR") return "fail"; 00260: if (s === "RUNNING") return "running"; 00261: if (s === "PENDING") return "pending"; 00262: return "missing"; 00263: } 00264: 00265: function setBadge(el, status, text) { 00266: if (!el) return; 00267: el.className = "badge " + getBadgeClass(status); 00268: el.textContent = text || safeUpper(status); 00269: } 00270: 00271: function normStageKey(value) { 00272: return String(value || "") 00273: .trim() 00274: .toLowerCase() 00275: .replace(/[^a-z0-9]+/g, "_") 00276: .replace(/^_+|_+$/g, ""); 00277: } 00278: 00279: function titleize(value) { 00280: return String(value || "") 00281: .replace(/[_-]+/g, " ") 00282: .replace(/\b\w/g, function (m) { return m.toUpperCase(); }); 00283: } 00284: 00285: async function getJson(name) { 00286: const res = await fetch(DATA_BASE + name, { cache: "no-store" }); 00287: if (!res.ok) throw new Error(name + " -> HTTP " + res.status); 00288: return res.json(); 00289: } 00290: 00291: async function getJsonOptional(name) { 00292: try { 00293: return await getJson(name); 00294: } catch (err) { 00295: return null; 00296: } 00297: } 00298: 00299: function openJson(label, payload) { 00300: if (els.drawerLabel) els.drawerLabel.textContent = label || "data"; 00301: if (els.jsonViewer) { 00302: try { 00303: els.jsonViewer.textContent = JSON.stringify(payload, null, 2); 00304: } catch (e) { 00305: els.jsonViewer.textContent = String(payload); 00306: } 00307: } 00308: if (els.dataViewerModal) { 00309: els.dataViewerModal.hidden = false; 00310: els.dataViewerModal.style.display = "block"; 00311: } 00312: } 00313: 00314: function donutMarkup(pct, label) { 00315: const value = Math.max(0, Math.min(100, Number(pct || 0))); 00316: const r = 54; 00317: const c = 2 * Math.PI * r; 00318: const dash = (value / 100) * c; 00319: return [ 00320: '
', 00321: '', 00325: '
', ===== function renderCenterCategory( ===== 00507: function selectedCategoryObject() { 00508: if (!state.selectedCategory) return null; 00509: const cats = (state.hubIndex && Array.isArray(state.hubIndex.categories)) ? state.hubIndex.categories : []; 00510: return cats.find(function (c) { 00511: return String(c.id || "") === String(state.selectedCategory); 00512: }) || null; 00513: } 00514: 00515: function renderCenterCategory(ph, cat) { 00516: if (!els.selectedStageName) return; 00517: if (els.selectedStageName) els.selectedStageName.textContent = cat.title || ph.name || "Category"; 00518: if (els.selectedStageDesc) els.selectedStageDesc.textContent = cat.sub || "Category view from canonical hub index."; 00519: if (els.selectedStageTotal) els.selectedStageTotal.innerHTML = ((cat.docs || []).length) + ' docs
in category'; 00520: if (els.selectedStageDonut) els.selectedStageDonut.innerHTML = donutMarkup(0, "category"); 00521: if (els.selectedPipelineSteps) { 00522: const docs = Array.isArray(cat.docs) ? cat.docs : []; 00523: els.selectedPipelineSteps.innerHTML = docs.length ? docs.map(function (doc) { 00524: return '
  • ' + (doc.title || doc.label || doc.id || "item") + '' + (doc.type || "doc") + '
  • '; 00525: }).join("") : '
  • No documents registeredMISSING
  • '; 00526: } 00527: } 00528: 00529: function renderCenterBucket(ph, cat, bucket) { 00530: if (els.selectedStageName) els.selectedStageName.textContent = (cat.title || ph.name || "Category") + " / " + titleize(bucket); 00531: if (els.selectedStageDesc) els.selectedStageDesc.textContent = "Bucket detail from canonical category documents."; 00532: const docs = Array.isArray(cat.docs) ? cat.docs : []; 00533: if (els.selectedStageTotal) els.selectedStageTotal.innerHTML = docs.length + ' docs
    in bucket view'; 00534: if (els.selectedStageDonut) els.selectedStageDonut.innerHTML = donutMarkup(0, titleize(bucket)); 00535: if (els.selectedPipelineSteps) { 00536: els.selectedPipelineSteps.innerHTML = docs.length ? docs.map(function (doc) { 00537: return [ 00538: '
  • ', 00539: '' + (doc.title || doc.label || doc.id || "item") + '', 00540: '' + (doc.type || "doc") + '', 00541: '
  • ' 00542: ].join(""); 00543: }).join("") : '
  • No documents registeredMISSING
  • '; 00544: } 00545: } 00546: 00547: function renderCanonicalSelection() { 00548: const ph = selectedPhaseObject(); 00549: const cat = selectedCategoryObject(); 00550: 00551: if (!ph) return; 00552: 00553: if (cat && state.selectedBucket) { 00554: renderCenterBucket(ph, cat, state.selectedBucket); 00555: return; 00556: } 00557: 00558: if (cat) { 00559: renderCenterCategory(ph, cat); 00560: return; 00561: } 00562: } 00563: 00564: 00565: function selectStage(stageKey) { 00566: state.selectedStage = normStageKey(stageKey); 00567: const items = ((state.contentIndex && state.contentIndex.content_index) || []); 00568: const navItem = items.find(function (item) { 00569: return normStageKey(item.stage_key) === state.selectedStage; 00570: }) || null; 00571: const rollup = stageRollup(state.selectedStage); 00572: const pct = rollup && typeof rollup.progress_pct === "number" ? Math.round(rollup.progress_pct) : 0; 00573: const totalSteps = rollup && Array.isArray(rollup.step_statuses) ? rollup.step_statuses.length : 0; 00574: 00575: if (els.heroTitle) els.heroTitle.textContent = navItem ? navItem.label : titleize(state.selectedStage); 00576: if (els.heroSubtitle) { 00577: els.heroSubtitle.textContent = "Runtime-backed stage view. Contract and runtime data are rendered from panel exports only."; 00578: } 00579: if (els.stageOrderBadge) { 00580: els.stageOrderBadge.textContent = navItem ? ("ORDER " + navItem.deployment_order) : "—"; 00581: } 00582: if (els.selectedStageName) { 00583: els.selectedStageName.textContent = navItem ? navItem.label : titleize(state.selectedStage); 00584: } 00585: if (els.selectedStageDesc) { 00586: els.selectedStageDesc.textContent = "Stage completion is read from runtime_status stage_rollup.progress_pct."; 00587: } 00588: if (els.selectedStageTotal) { 00589: els.selectedStageTotal.innerHTML = totalSteps 00590: ? (totalSteps + ' required steps
    in contract') 00591: : 'MISSING stage rollup
    in runtime'; 00592: } 00593: if (els.selectedStageDonut) { 00594: els.selectedStageDonut.innerHTML = donutMarkup(pct, "stage completion"); 00595: } 00596: if (els.selectedRuntimeBadge) { 00597: setBadge(els.selectedRuntimeBadge, rollup ? runtimeStatusOf(rollup) : "MISSING", rollup ? runtimeStatusOf(rollup) : "MISSING"); 00598: } 00599: 00600: renderSelectedPipeline(state.selectedStage, rollup); 00601: renderCanonicalSelection(); 00602: 00603: document.querySelectorAll(".stage-btn").forEach(function (btn) { 00604: btn.classList.toggle("active", btn.dataset.stageKey === state.selectedStage); 00605: }); 00606: } 00607: 00608: function wireButtons() { 00609: [ 00610: ["Manifest", els.btnOpenManifest, state.manifest], 00611: ["Project Progress", els.btnOpenContract, state.projectProgress], 00612: ["Host Runtime", els.btnOpenHost, state.hostRuntime], 00613: ["Docker Runtime", els.btnOpenDocker, state.dockerRuntime], 00614: ["Runtime Status", els.btnOpenRuntime, state.runtimeStatus] 00615: ].forEach(function (entry) { 00616: const label = entry[0]; 00617: const btn = entry[1]; 00618: const payload = entry[2]; 00619: if (!btn) return; 00620: btn.onclick = function () { 00621: openJson(label, payload || { status: "MISSING" }); 00622: }; 00623: }); 00624: } 00625: 00626: async function boot() { 00627: try { 00628: const core = await Promise.all([ 00629: getJsonOptional("panel_manifest.json"), 00630: getJsonOptional("hub_index.json"), 00631: getJsonOptional("panel_content_index.json"), 00632: getJsonOptional("panel_navigation_spec.json"), 00633: getJsonOptional("subcategory_pipelines.json"), 00634: getJsonOptional("project_progress.json"), 00635: getJsonOptional("host_runtime.json"), 00636: getJsonOptional("docker_runtime.json"), 00637: getJsonOptional("runtime_status.json") 00638: ]); 00639: 00640: state.manifest = core[0]; 00641: state.hubIndex = core[1]; 00642: state.contentIndex = core[2]; 00643: state.navigationSpec = core[3]; 00644: state.pipelines = core[4]; 00645: state.projectProgress = core[5]; 00646: state.hostRuntime = core[6]; 00647: state.dockerRuntime = core[7]; 00648: state.runtimeStatus = core[8]; 00649: 00650: renderGlobal(); 00651: renderCanonicalNav(); 00652: renderHostRuntime(); 00653: renderDockerRuntime(); 00654: renderPipelineBoard(); ===== function renderCenterBucket( ===== 00521: if (els.selectedPipelineSteps) { 00522: const docs = Array.isArray(cat.docs) ? cat.docs : []; 00523: els.selectedPipelineSteps.innerHTML = docs.length ? docs.map(function (doc) { 00524: return '
  • ' + (doc.title || doc.label || doc.id || "item") + '' + (doc.type || "doc") + '
  • '; 00525: }).join("") : '
  • No documents registeredMISSING
  • '; 00526: } 00527: } 00528: 00529: function renderCenterBucket(ph, cat, bucket) { 00530: if (els.selectedStageName) els.selectedStageName.textContent = (cat.title || ph.name || "Category") + " / " + titleize(bucket); 00531: if (els.selectedStageDesc) els.selectedStageDesc.textContent = "Bucket detail from canonical category documents."; 00532: const docs = Array.isArray(cat.docs) ? cat.docs : []; 00533: if (els.selectedStageTotal) els.selectedStageTotal.innerHTML = docs.length + ' docs
    in bucket view'; 00534: if (els.selectedStageDonut) els.selectedStageDonut.innerHTML = donutMarkup(0, titleize(bucket)); 00535: if (els.selectedPipelineSteps) { 00536: els.selectedPipelineSteps.innerHTML = docs.length ? docs.map(function (doc) { 00537: return [ 00538: '
  • ', 00539: '' + (doc.title || doc.label || doc.id || "item") + '', 00540: '' + (doc.type || "doc") + '', 00541: '
  • ' 00542: ].join(""); 00543: }).join("") : '
  • No documents registeredMISSING
  • '; 00544: } 00545: } 00546: 00547: function renderCanonicalSelection() { 00548: const ph = selectedPhaseObject(); 00549: const cat = selectedCategoryObject(); 00550: 00551: if (!ph) return; 00552: 00553: if (cat && state.selectedBucket) { 00554: renderCenterBucket(ph, cat, state.selectedBucket); 00555: return; 00556: } 00557: 00558: if (cat) { 00559: renderCenterCategory(ph, cat); 00560: return; 00561: } 00562: } 00563: 00564: 00565: function selectStage(stageKey) { 00566: state.selectedStage = normStageKey(stageKey); 00567: const items = ((state.contentIndex && state.contentIndex.content_index) || []); 00568: const navItem = items.find(function (item) { 00569: return normStageKey(item.stage_key) === state.selectedStage; 00570: }) || null; 00571: const rollup = stageRollup(state.selectedStage); 00572: const pct = rollup && typeof rollup.progress_pct === "number" ? Math.round(rollup.progress_pct) : 0; 00573: const totalSteps = rollup && Array.isArray(rollup.step_statuses) ? rollup.step_statuses.length : 0; 00574: 00575: if (els.heroTitle) els.heroTitle.textContent = navItem ? navItem.label : titleize(state.selectedStage); 00576: if (els.heroSubtitle) { 00577: els.heroSubtitle.textContent = "Runtime-backed stage view. Contract and runtime data are rendered from panel exports only."; 00578: } 00579: if (els.stageOrderBadge) { 00580: els.stageOrderBadge.textContent = navItem ? ("ORDER " + navItem.deployment_order) : "—"; 00581: } 00582: if (els.selectedStageName) { 00583: els.selectedStageName.textContent = navItem ? navItem.label : titleize(state.selectedStage); 00584: } 00585: if (els.selectedStageDesc) { 00586: els.selectedStageDesc.textContent = "Stage completion is read from runtime_status stage_rollup.progress_pct."; 00587: } 00588: if (els.selectedStageTotal) { 00589: els.selectedStageTotal.innerHTML = totalSteps 00590: ? (totalSteps + ' required steps
    in contract') 00591: : 'MISSING stage rollup
    in runtime'; 00592: } 00593: if (els.selectedStageDonut) { 00594: els.selectedStageDonut.innerHTML = donutMarkup(pct, "stage completion"); 00595: } 00596: if (els.selectedRuntimeBadge) { 00597: setBadge(els.selectedRuntimeBadge, rollup ? runtimeStatusOf(rollup) : "MISSING", rollup ? runtimeStatusOf(rollup) : "MISSING"); 00598: } 00599: 00600: renderSelectedPipeline(state.selectedStage, rollup); 00601: renderCanonicalSelection(); 00602: 00603: document.querySelectorAll(".stage-btn").forEach(function (btn) { 00604: btn.classList.toggle("active", btn.dataset.stageKey === state.selectedStage); 00605: }); 00606: } 00607: 00608: function wireButtons() { 00609: [ 00610: ["Manifest", els.btnOpenManifest, state.manifest], 00611: ["Project Progress", els.btnOpenContract, state.projectProgress], 00612: ["Host Runtime", els.btnOpenHost, state.hostRuntime], 00613: ["Docker Runtime", els.btnOpenDocker, state.dockerRuntime], 00614: ["Runtime Status", els.btnOpenRuntime, state.runtimeStatus] 00615: ].forEach(function (entry) { 00616: const label = entry[0]; 00617: const btn = entry[1]; 00618: const payload = entry[2]; 00619: if (!btn) return; 00620: btn.onclick = function () { 00621: openJson(label, payload || { status: "MISSING" }); 00622: }; 00623: }); 00624: } 00625: 00626: async function boot() { 00627: try { 00628: const core = await Promise.all([ 00629: getJsonOptional("panel_manifest.json"), 00630: getJsonOptional("hub_index.json"), 00631: getJsonOptional("panel_content_index.json"), 00632: getJsonOptional("panel_navigation_spec.json"), 00633: getJsonOptional("subcategory_pipelines.json"), 00634: getJsonOptional("project_progress.json"), 00635: getJsonOptional("host_runtime.json"), 00636: getJsonOptional("docker_runtime.json"), 00637: getJsonOptional("runtime_status.json") 00638: ]); 00639: 00640: state.manifest = core[0]; 00641: state.hubIndex = core[1]; 00642: state.contentIndex = core[2]; 00643: state.navigationSpec = core[3]; 00644: state.pipelines = core[4]; 00645: state.projectProgress = core[5]; 00646: state.hostRuntime = core[6]; 00647: state.dockerRuntime = core[7]; 00648: state.runtimeStatus = core[8]; 00649: 00650: renderGlobal(); 00651: renderCanonicalNav(); 00652: renderHostRuntime(); 00653: renderDockerRuntime(); 00654: renderPipelineBoard(); 00655: 00656: const firstPhase = ((state.hubIndex && state.hubIndex.phases) || [])[0] || null; 00657: 00658: if (firstPhase) { 00659: const firstPhaseKey = normStageKey(firstPhase.id || firstPhase.stage_key || ""); 00660: state.openPhases = new Set([firstPhaseKey]); 00661: state.selectedStage = firstPhaseKey; 00662: renderCanonicalNav(); 00663: selectStage(firstPhaseKey); 00664: } 00665: 00666: wireButtons(); 00667: 00668: window.__devonPanelDebug = { ===== function selectStage( ===== 00557: 00558: if (cat) { 00559: renderCenterCategory(ph, cat); 00560: return; 00561: } 00562: } 00563: 00564: 00565: function selectStage(stageKey) { 00566: state.selectedStage = normStageKey(stageKey); 00567: const items = ((state.contentIndex && state.contentIndex.content_index) || []); 00568: const navItem = items.find(function (item) { 00569: return normStageKey(item.stage_key) === state.selectedStage; 00570: }) || null; 00571: const rollup = stageRollup(state.selectedStage); 00572: const pct = rollup && typeof rollup.progress_pct === "number" ? Math.round(rollup.progress_pct) : 0; 00573: const totalSteps = rollup && Array.isArray(rollup.step_statuses) ? rollup.step_statuses.length : 0; 00574: 00575: if (els.heroTitle) els.heroTitle.textContent = navItem ? navItem.label : titleize(state.selectedStage); 00576: if (els.heroSubtitle) { 00577: els.heroSubtitle.textContent = "Runtime-backed stage view. Contract and runtime data are rendered from panel exports only."; 00578: } 00579: if (els.stageOrderBadge) { 00580: els.stageOrderBadge.textContent = navItem ? ("ORDER " + navItem.deployment_order) : "—"; 00581: } 00582: if (els.selectedStageName) { 00583: els.selectedStageName.textContent = navItem ? navItem.label : titleize(state.selectedStage); 00584: } 00585: if (els.selectedStageDesc) { 00586: els.selectedStageDesc.textContent = "Stage completion is read from runtime_status stage_rollup.progress_pct."; 00587: } 00588: if (els.selectedStageTotal) { 00589: els.selectedStageTotal.innerHTML = totalSteps 00590: ? (totalSteps + ' required steps
    in contract') 00591: : 'MISSING stage rollup
    in runtime'; 00592: } 00593: if (els.selectedStageDonut) { 00594: els.selectedStageDonut.innerHTML = donutMarkup(pct, "stage completion"); 00595: } 00596: if (els.selectedRuntimeBadge) { 00597: setBadge(els.selectedRuntimeBadge, rollup ? runtimeStatusOf(rollup) : "MISSING", rollup ? runtimeStatusOf(rollup) : "MISSING"); 00598: } 00599: 00600: renderSelectedPipeline(state.selectedStage, rollup); 00601: renderCanonicalSelection(); 00602: 00603: document.querySelectorAll(".stage-btn").forEach(function (btn) { 00604: btn.classList.toggle("active", btn.dataset.stageKey === state.selectedStage); 00605: }); 00606: } 00607: 00608: function wireButtons() { 00609: [ 00610: ["Manifest", els.btnOpenManifest, state.manifest], 00611: ["Project Progress", els.btnOpenContract, state.projectProgress], 00612: ["Host Runtime", els.btnOpenHost, state.hostRuntime], 00613: ["Docker Runtime", els.btnOpenDocker, state.dockerRuntime], 00614: ["Runtime Status", els.btnOpenRuntime, state.runtimeStatus] 00615: ].forEach(function (entry) { 00616: const label = entry[0]; 00617: const btn = entry[1]; 00618: const payload = entry[2]; 00619: if (!btn) return; 00620: btn.onclick = function () { 00621: openJson(label, payload || { status: "MISSING" }); 00622: }; 00623: }); 00624: } 00625: 00626: async function boot() { 00627: try { 00628: const core = await Promise.all([ 00629: getJsonOptional("panel_manifest.json"), 00630: getJsonOptional("hub_index.json"), 00631: getJsonOptional("panel_content_index.json"), 00632: getJsonOptional("panel_navigation_spec.json"), 00633: getJsonOptional("subcategory_pipelines.json"), 00634: getJsonOptional("project_progress.json"), 00635: getJsonOptional("host_runtime.json"), 00636: getJsonOptional("docker_runtime.json"), 00637: getJsonOptional("runtime_status.json") 00638: ]); 00639: 00640: state.manifest = core[0]; 00641: state.hubIndex = core[1]; 00642: state.contentIndex = core[2]; 00643: state.navigationSpec = core[3]; 00644: state.pipelines = core[4]; 00645: state.projectProgress = core[5]; 00646: state.hostRuntime = core[6]; 00647: state.dockerRuntime = core[7]; 00648: state.runtimeStatus = core[8]; 00649: 00650: renderGlobal(); 00651: renderCanonicalNav(); 00652: renderHostRuntime(); 00653: renderDockerRuntime(); 00654: renderPipelineBoard(); 00655: 00656: const firstPhase = ((state.hubIndex && state.hubIndex.phases) || [])[0] || null; 00657: 00658: if (firstPhase) { 00659: const firstPhaseKey = normStageKey(firstPhase.id || firstPhase.stage_key || ""); 00660: state.openPhases = new Set([firstPhaseKey]); 00661: state.selectedStage = firstPhaseKey; 00662: renderCanonicalNav(); 00663: selectStage(firstPhaseKey); 00664: } 00665: 00666: wireButtons(); 00667: 00668: window.__devonPanelDebug = { 00669: state: state, 00670: selectStage: selectStage 00671: }; 00672: } catch (err) { 00673: if (els.heroTitle) els.heroTitle.textContent = "Panel bootstrap failed"; 00674: if (els.heroSubtitle) els.heroSubtitle.textContent = String(err && (err.message || err) || err); 00675: if (els.jsonViewer) els.jsonViewer.textContent = String(err && (err.stack || err.message) || err); 00676: console.error("[DEVON_PANEL_BOOT]", err); 00677: } 00678: } 00679: 00680: boot(); 00681: })(); 00682: 00683: 00684: document.getElementById('btn-monitor')?.addEventListener('click', () => { 00685: window.open('../monitor/', '_blank'); 00686: }); ===== function stageRollup( ===== 00362: 00363: function stageRows(stageKey) { 00364: const snapshot = ((state.runtimeStatus && state.runtimeStatus.runtime_snapshot) || []); 00365: return snapshot.filter(function (row) { 00366: return normStageKey(row.deployment_stage) === normStageKey(stageKey); 00367: }); 00368: } 00369: 00370: function stageRollup(stageKey) { 00371: return stageRows(stageKey).find(function (row) { 00372: return String(row.row_kind || "") === "stage_rollup"; 00373: }) || null; 00374: } 00375: 00376: function renderGlobal() { 00377: const snapshot = ((state.runtimeStatus && state.runtimeStatus.runtime_snapshot) || []); 00378: const counts = snapshot.reduce(function (acc, row) { 00379: const s = runtimeStatusOf(row); 00380: acc[s] = (acc[s] || 0) + 1; 00381: return acc; 00382: }, { PASS: 0, FAIL: 0, MISSING: 0, RUNNING: 0, PENDING: 0 }); 00383: 00384: if (els.globalPills) { 00385: els.globalPills.innerHTML = [ 00386: 'PASS ' + (counts.PASS || 0) + '', 00387: 'FAIL ' + (counts.FAIL || 0) + '', 00388: 'MISSING ' + (counts.MISSING || 0) + '', 00389: 'RUNNING ' + (counts.RUNNING || 0) + '', 00390: 'PENDING ' + (counts.PENDING || 0) + '' 00391: ].join(""); 00392: } 00393: 00394: const gp = state.projectProgress && state.projectProgress.global_project_progress; 00395: if (els.projectDonut) { 00396: if (gp && typeof gp.progress_pct === "number") { 00397: els.projectDonut.innerHTML = donutMarkup(Math.round(gp.progress_pct), "project completion"); 00398: } else { 00399: els.projectDonut.innerHTML = missingDonutMarkup("project completion"); 00400: } 00401: } 00402: } 00403: 00404: function renderNav() { 00405: renderCanonicalNav(); 00406: } 00407: 00408: function renderHostRuntime() { 00409: const host = (state.hostRuntime && state.hostRuntime.host_snapshot) || {}; 00410: setBadge(els.hostStatusBadge, host.overall_status || "MISSING", safeUpper(host.overall_status || "MISSING")); 00411: 00412: const cpu = host.cpu || {}; 00413: const memory = host.memory || {}; 00414: const disk = host.disk || {}; 00415: const load = host.load || {}; 00416: 00417: if (els.hostRuntimeGrid) { 00418: els.hostRuntimeGrid.innerHTML = [ 00419: metricBox("CPU", valueOrDash(cpu.usage_pct, "%"), "cores: " + valueOrDash(cpu.core_count, "")), 00420: metricBox("Memory", valueOrDash(memory.usage_pct, "%"), valueOrDash(memory.used_mb, " MB") + " / " + valueOrDash(memory.total_mb, " MB")), 00421: metricBox("Disk", valueOrDash(disk.usage_pct, "%"), valueOrDash(disk.used_gb, " GB") + " / " + valueOrDash(disk.total_gb, " GB")), 00422: metricBox("Load 1m", valueOrDash(load.load_1m, ""), "5m: " + valueOrDash(load.load_5m, "") + " | 15m: " + valueOrDash(load.load_15m, "")) 00423: ].join(""); 00424: } 00425: } 00426: 00427: function renderDockerRuntime() { 00428: const runtime = (state.dockerRuntime && state.dockerRuntime.runtime_snapshot) || {}; 00429: setBadge(els.dockerStatusBadge, runtime.overall_status || "MISSING", safeUpper(runtime.overall_status || "MISSING")); 00430: 00431: const engine = runtime.docker_engine || {}; 00432: const compose = runtime.compose || {}; 00433: const images = runtime.images || {}; 00434: const volumes = runtime.volumes || {}; 00435: const networks = runtime.networks || {}; 00436: const containers = Array.isArray(runtime.containers) ? runtime.containers : []; 00437: 00438: if (els.dockerRuntimeGrid) { 00439: els.dockerRuntimeGrid.innerHTML = [ 00440: metricBox("Engine", engine.installed === true ? "installed" : (engine.installed === false ? "not installed" : "MISSING"), "version: " + valueOrDash(engine.version, "")), 00441: metricBox("Compose", compose.installed === true ? "installed" : (compose.installed === false ? "not installed" : "MISSING"), "version: " + valueOrDash(compose.version, "")), 00442: metricBox("Containers", String(containers.length), "observable list"), 00443: metricBox("Images", valueOrDash(images.total, ""), "Volumes: " + valueOrDash(volumes.total, "") + " | Networks: " + valueOrDash(networks.total, "")) 00444: ].join(""); 00445: } 00446: } 00447: 00448: function renderPipelineBoard() { 00449: const items = ((state.runtimeStatus && state.runtimeStatus.runtime_snapshot) || []); 00450: if (els.pipelineRuntimeCount) { 00451: els.pipelineRuntimeCount.textContent = items.length + " runtime rows"; 00452: } 00453: if (!els.pipelineRuntimeBoard) return; 00454: if (!items.length) { 00455: els.pipelineRuntimeBoard.innerHTML = '

    No runtime rows

    Status is MISSING.

    '; 00456: return; 00457: } 00458: els.pipelineRuntimeBoard.innerHTML = items.slice(0, 24).map(function (item) { 00459: const status = runtimeStatusOf(item); 00460: return [ 00461: '
    ', 00462: '

    ' + titleize(item.deployment_stage || "runtime") + ' / ' + titleize(item.technology || item.subcategory || "item") + '

    ', 00463: '

    Kind: ' + titleize(item.row_kind || "runtime_row") + '

    ', 00464: '
    ', 00465: '' + status + '', 00466: '' + valueOrDash(item.progress_pct, "%") + '', 00467: '
    ', 00468: '
    ' 00469: ].join(""); 00470: }).join(""); 00471: } 00472: 00473: function renderSelectedPipeline(stageKey, rollup) { 00474: if (!els.selectedPipelineSteps) return; 00475: const pipelines = (state.pipelines && state.pipelines.pipelines) || {}; 00476: const contract = pipelines[stageKey] || {}; 00477: const seq = Array.isArray(contract.sequence) ? contract.sequence : []; 00478: const stepStatuses = rollup && Array.isArray(rollup.step_statuses) ? rollup.step_statuses : []; 00479: 00480: if (!seq.length) { 00481: els.selectedPipelineSteps.innerHTML = '
  • No pipeline contract foundmissing
  • '; 00482: return; 00483: } 00484: 00485: els.selectedPipelineSteps.innerHTML = seq.map(function (step) { 00486: const observed = stepStatuses.find(function (x) { 00487: return normStageKey(x.step || x.name) === normStageKey(step); 00488: }); 00489: const status = observed ? safeUpper(observed.status) : "MISSING"; 00490: return [ 00491: '
  • ', 00492: '' + titleize(step) + '', 00493: '' + status + '', 00494: '
  • ' 00495: ].join(""); 00496: }).join(""); 00497: } 00498: 00499: 00500: function selectedPhaseObject() { 00501: const phases = (state.hubIndex && Array.isArray(state.hubIndex.phases)) ? state.hubIndex.phases : []; 00502: return phases.find(function (p) { 00503: return normStageKey(p.id || p.stage_key || "") === normStageKey(state.selectedStage); 00504: }) || null; 00505: } 00506: 00507: function selectedCategoryObject() { 00508: if (!state.selectedCategory) return null; 00509: const cats = (state.hubIndex && Array.isArray(state.hubIndex.categories)) ? state.hubIndex.categories : [];