261 return "missing";
262 }
263
264 function setBadge(el, status, text) {
265 if (!el) return;
266 el.className = "badge " + getBadgeClass(status);
267 el.textContent = text || safeUpper(status);
268 }
269
270 function normStageKey(value) {
271 return String(value || "")
272 .trim()
273 .toLowerCase()
274 .replace(/[^a-z0-9]+/g, "_")
275 .replace(/^_+|_+$/g, "");
276 }
277
278 function titleize(value) {
279 return String(value || "")
280 .replace(/[_-]+/g, " ")
281 .replace(/\b\w/g, function (m) { return m.toUpperCase(); });
282 }
283
284 async function getJson(name) {
285 const res = await fetch(DATA_BASE + name, { cache: "no-store" });
286 if (!res.ok) throw new Error(name + " -> HTTP " + res.status);
287 return res.json();
288 }
289
290 async function getJsonOptional(name) {
291 try {
292 return await getJson(name);
293 } catch (err) {
294 return null;
295 }
296 }
297
298 function openJson(label, payload) {
299 if (els.drawerLabel) els.drawerLabel.textContent = label || "data";
300 if (els.jsonViewer) {
301 try {
302 els.jsonViewer.textContent = JSON.stringify(payload, null, 2);
303 } catch (e) {
304 els.jsonViewer.textContent = String(payload);
305 }
306 }
307 if (els.dataViewerModal) {
308 els.dataViewerModal.hidden = false;
309 els.dataViewerModal.style.display = "block";
310 }
311 }
312
313 function donutMarkup(pct, label) {
314 const value = Math.max(0, Math.min(100, Number(pct || 0)));
315 const r = 54;
316 const c = 2 * Math.PI * r;
317 const dash = (value / 100) * c;
318 return [
319 '
',
320 '
',
324 '
',
325 '' + value + '%',
326 '' + (label || "progress") + '',
327 '
',
328 '
'
329 ].join("");
330 }
331
332 function missingDonutMarkup(label) {
333 return donutMarkup(0, label || "missing");
334 }
335
336 function metricBox(label, value, sub) {
337 return [
338 '',
339 '
' + label + '
',
340 '
' + value + '
',
341 '
' + sub + '
',
342 '
'
343 ].join("");
344 }
345
346 function valueOrDash(value, suffix) {
347 if (value === null || value === undefined || value === "") return "—";
348 return String(value) + String(suffix || "");
349 }
350
351 function runtimeStatusOf(row) {
352 if (!row || typeof row !== "object") return "MISSING";
353 return safeUpper(
354 row.runtime_status ??
355 row.status ??
356 row.state ??
357 row.overall_status ??
358 "MISSING"
359 );
360 }
361
362 function stageRows(stageKey) {
363 const snapshot = ((state.runtimeStatus && state.runtimeStatus.runtime_snapshot) || []);
364 return snapshot.filter(function (row) {
365 return normStageKey(row.deployment_stage) === normStageKey(stageKey);
366 });
367 }
368
369 function stageRollup(stageKey) {
370 return stageRows(stageKey).find(function (row) {
371 return String(row.row_kind || "") === "stage_rollup";
372 }) || null;
373 }
374
375 function renderGlobal() {
376 const snapshot = ((state.runtimeStatus && state.runtimeStatus.runtime_snapshot) || []);
377 const counts = snapshot.reduce(function (acc, row) {
378 const s = runtimeStatusOf(row);
379 acc[s] = (acc[s] || 0) + 1;
380 return acc;
381 }, { PASS: 0, FAIL: 0, MISSING: 0, RUNNING: 0, PENDING: 0 });
382
383 if (els.globalPills) {
384 els.globalPills.innerHTML = [
385 'PASS ' + (counts.PASS || 0) + '',
386 'FAIL ' + (counts.FAIL || 0) + '',
387 'MISSING ' + (counts.MISSING || 0) + '',
388 'RUNNING ' + (counts.RUNNING || 0) + '',
389 'PENDING ' + (counts.PENDING || 0) + ''
390 ].join("");
391 }
392
393 const gp = state.projectProgress && state.projectProgress.global_project_progress;
394 if (els.projectDonut) {
395 if (gp && typeof gp.progress_pct === "number") {
396 els.projectDonut.innerHTML = donutMarkup(Math.round(gp.progress_pct), "project completion");
397 } else {
398 els.projectDonut.innerHTML = missingDonutMarkup("project completion");
399 }
400 }
401 }
402
403 function renderNav() {
404 renderCanonicalNav();
405 }
406
407 function renderHostRuntime() {
408 const host = (state.hostRuntime && state.hostRuntime.host_snapshot) || {};
409 setBadge(els.hostStatusBadge, host.overall_status || "MISSING", safeUpper(host.overall_status || "MISSING"));
410
411 const cpu = host.cpu || {};
412 const memory = host.memory || {};
413 const disk = host.disk || {};
414 const load = host.load || {};
415
416 if (els.hostRuntimeGrid) {
417 els.hostRuntimeGrid.innerHTML = [
418 metricBox("CPU", valueOrDash(cpu.usage_pct, "%"), "cores: " + valueOrDash(cpu.core_count, "")),
419 metricBox("Memory", valueOrDash(memory.usage_pct, "%"), valueOrDash(memory.used_mb, " MB") + " / " + valueOrDash(memory.total_mb, " MB")),
420 metricBox("Disk", valueOrDash(disk.usage_pct, "%"), valueOrDash(disk.used_gb, " GB") + " / " + valueOrDash(disk.total_gb, " GB")),
421 metricBox("Load 1m", valueOrDash(load.load_1m, ""), "5m: " + valueOrDash(load.load_5m, "") + " | 15m: " + valueOrDash(load.load_15m, ""))
422 ].join("");
423 }
424 }
425
426 function renderDockerRuntime() {
427 const runtime = (state.dockerRuntime && state.dockerRuntime.runtime_snapshot) || {};
428 setBadge(els.dockerStatusBadge, runtime.overall_status || "MISSING", safeUpper(runtime.overall_status || "MISSING"));
429
430 const engine = runtime.docker_engine || {};
431 const compose = runtime.compose || {};
432 const images = runtime.images || {};
433 const volumes = runtime.volumes || {};
434 const networks = runtime.networks || {};
435 const containers = Array.isArray(runtime.containers) ? runtime.containers : [];
436
437 if (els.dockerRuntimeGrid) {
438 els.dockerRuntimeGrid.innerHTML = [
439 metricBox("Engine", engine.installed === true ? "installed" : (engine.installed === false ? "not installed" : "MISSING"), "version: " + valueOrDash(engine.version, "")),
440 metricBox("Compose", compose.installed === true ? "installed" : (compose.installed === false ? "not installed" : "MISSING"), "version: " + valueOrDash(compose.version, "")),
441 metricBox("Containers", String(containers.length), "observable list"),
442 metricBox("Images", valueOrDash(images.total, ""), "Volumes: " + valueOrDash(volumes.total, "") + " | Networks: " + valueOrDash(networks.total, ""))
443 ].join("");
444 }
445 }
446
447 function renderPipelineBoard() {
448 const items = ((state.runtimeStatus && state.runtimeStatus.runtime_snapshot) || []);
449 if (els.pipelineRuntimeCount) {
450 els.pipelineRuntimeCount.textContent = items.length + " runtime rows";
451 }
452 if (!els.pipelineRuntimeBoard) return;
453 if (!items.length) {
454 els.pipelineRuntimeBoard.innerHTML = 'No runtime rows
Status is MISSING.
';
455 return;
456 }
457 els.pipelineRuntimeBoard.innerHTML = items.slice(0, 24).map(function (item) {
458 const status = runtimeStatusOf(item);
459 return [
460 '',
461 '
' + titleize(item.deployment_stage || "runtime") + ' / ' + titleize(item.technology || item.subcategory || "item") + '
',
462 '
Kind: ' + titleize(item.row_kind || "runtime_row") + '
',
463 '
',
464 '' + status + '',
465 '' + valueOrDash(item.progress_pct, "%") + '',
466 '
',
467 '
'
468 ].join("");
469 }).join("");
470 }
471
472 function renderSelectedPipeline(stageKey, rollup) {
473 if (!els.selectedPipelineSteps) return;
474 const pipelines = (state.pipelines && state.pipelines.pipelines) || {};
475 const contract = pipelines[stageKey] || {};
476 const seq = Array.isArray(contract.sequence) ? contract.sequence : [];
477 const stepStatuses = rollup && Array.isArray(rollup.step_statuses) ? rollup.step_statuses : [];
478
479 if (!seq.length) {
480 els.selectedPipelineSteps.innerHTML = 'No pipeline contract foundmissing';
481 return;
482 }
483
484 els.selectedPipelineSteps.innerHTML = seq.map(function (step) {
485 const observed = stepStatuses.find(function (x) {
486 return normStageKey(x.step || x.name) === normStageKey(step);
487 });
488 const status = observed ? safeUpper(observed.status) : "MISSING";
489 return [
490 '',
491 '' + titleize(step) + '',
492 '' + status + '',
493 ''
494 ].join("");
495 }).join("");
496 }
497
498
499
500 function hasRuntimeStageKey(stageKey) {
501 const key = normStageKey(stageKey);
502 const items = ((state.runtimeStatus && state.runtimeStatus.runtime_snapshot) || []);
503 return items.some(function (row) {
504 return String(row && row.row_kind || "") === "stage_rollup"
505 && normStageKey(row && row.deployment_stage) === key;
506 });
507 }
508
509 function hasPipelineStageKey(stageKey) {
510 const key = normStageKey(stageKey);
511 const pipelines = (state.subcategoryPipelines && state.subcategoryPipelines.pipelines) || {};
512 return Object.keys(pipelines).some(function (k) {
513 return normStageKey(k) === key;
514 });
515 }
516
517
518 function isCanonicalPhaseKey(stageKey) {
519 return /^phase-\d+$/i.test(String(stageKey || "").trim());
520 }