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 }