// ═══════════════════════════════════════════════ // Ced's Portfolio — script.js // NOC data loads from data/noc-status.json // Refreshes every 30s. Graceful fallback on fail. // ═══════════════════════════════════════════════ document.addEventListener("DOMContentLoaded", () => { // Set copyright year const yearSpan = document.querySelector("[data-year]"); if (yearSpan) yearSpan.textContent = new Date().getFullYear(); // NOC data loadNocStatus(); setInterval(loadNocStatus, 30000); // GitHub pinned repos loadGithubRepos(); }); // Track consecutive failures so we can show the fallback let failCount = 0; const MAX_FAILS = 3; async function loadNocStatus() { const dot = document.getElementById("noc-dot"); const statusText = document.getElementById("noc-status-text"); const badge = document.getElementById("noc-refresh-badge"); const nodes = document.getElementById("metric-nodes"); const services = document.getElementById("metric-services"); const vlans = document.getElementById("metric-vlans"); const edge = document.getElementById("metric-edge"); const grid = document.getElementById("noc-system-grid"); const alertList = document.getElementById("noc-alert-list"); const fallback = document.getElementById("noc-fallback"); try { // Cache-bust to always get fresh data const res = await fetch(`data/noc-status.json?t=${Date.now()}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); // Reset fail counter on success failCount = 0; // ── Status bar ── if (dot) { dot.className = "status-dot live"; } if (statusText) { statusText.textContent = `${data.nocName} — ${data.status} — Last updated: ${data.lastUpdated}`; } if (badge) { badge.textContent = `Auto-refresh: 30s`; } // Hide fallback if it was showing if (fallback) fallback.style.display = "none"; // ── Metrics ── if (nodes) animateMetric(nodes, data.summary.nodes); if (services) animateMetric(services, data.summary.services); if (vlans) animateMetric(vlans, data.summary.vlans); if (edge) animateMetric(edge, data.summary.edgeSystems); // ── System cards ── if (grid) { grid.innerHTML = ""; data.systems.forEach((system) => { const card = document.createElement("article"); card.className = "noc-system-card"; const rawStatus = system.status.toLowerCase(); const statusClass = rawStatus.includes("online") || rawStatus.includes("operational") ? "online" : rawStatus.includes("building") || rawStatus.includes("tuning") || rawStatus.includes("active") ? "warning" : "offline"; card.innerHTML = `
${system.layer}
${system.label || "System"}
${system.severity || "Unknown"}

${system.name}

${system.type}

${system.address}
Status: ${system.status} Latency: ${system.latency || "N/A"} Last Check: ${system.lastCheck || "Manual"}
`; grid.appendChild(card); }); } // ── Alerts ── if (alertList) renderAlerts(data.systems, alertList); } catch (err) { failCount++; console.warn(`[NOC] Fetch failed (${failCount}/${MAX_FAILS}):`, err.message); // Update dot to error state if (dot) dot.className = "status-dot error"; if (statusText) { statusText.textContent = failCount >= MAX_FAILS ? "NOC feed offline — view live dashboard at noc.chasedumphord.com" : `NOC data unavailable — retrying... (${failCount}/${MAX_FAILS})`; } if (badge) badge.textContent = ""; // After MAX_FAILS attempts, show the fallback block if (failCount >= MAX_FAILS && fallback) { fallback.style.display = "block"; } } } // Animate a number metric with a quick pop function animateMetric(el, value) { el.classList.remove("metric-pop"); el.textContent = value; void el.offsetWidth; // force reflow el.classList.add("metric-pop"); } // ═══════════════════════════════════════════════ // GitHub Repos — fetches public repos for ced4568 // Sorted by last pushed, capped at 6 cards // ═══════════════════════════════════════════════ // Language → color map (subset of GitHub's palette) const LANG_COLORS = { Python: "#3572A5", JavaScript: "#f1e05a", Shell: "#89e051", HTML: "#e34c26", CSS: "#563d7c", Dockerfile: "#384d54", YAML: "#cb171e", Makefile: "#427819", }; async function loadGithubRepos() { const container = document.getElementById("github-repos"); if (!container) return; try { const res = await fetch( "https://api.github.com/users/ced4568/repos?sort=pushed&per_page=6", { headers: { Accept: "application/vnd.github+json" } } ); if (!res.ok) throw new Error(`GitHub API ${res.status}`); const repos = await res.json(); // Filter out forks for cleaner display const ownRepos = repos.filter((r) => !r.fork).slice(0, 6); if (ownRepos.length === 0) { container.innerHTML = `

No public repositories found.

`; return; } container.innerHTML = ownRepos.map((repo) => { const langColor = LANG_COLORS[repo.language] || "var(--accent-2)"; const desc = repo.description ? repo.description : "No description provided."; const updatedDate = new Date(repo.pushed_at).toLocaleDateString("en-US", { month: "short", year: "numeric", }); return `
📁 ${repo.name} ${repo.private ? "private" : "public"}

${desc}

${repo.language ? ` ${repo.language} ` : ""} ★ ${repo.stargazers_count} Updated ${updatedDate}
`; }).join(""); } catch (err) { console.warn("[GitHub] Failed to load repos:", err.message); if (container) { container.innerHTML = `

Could not load repositories. View on GitHub →

`; } } } function renderAlerts(systems, alertList) { const scored = systems .map((sys) => { const sev = (sys.severity || "").toLowerCase(); const status = (sys.status || "").toLowerCase(); let score = 0; if (sev.includes("critical") || status.includes("offline")) score = 3; else if (sev.includes("degraded")) score = 2; else if (sev.includes("unknown")) score = 1; return { ...sys, score }; }) .filter((s) => s.score > 0) .sort((a, b) => b.score - a.score) .slice(0, 3); if (scored.length === 0) { alertList.innerHTML = `
All monitored systems nominal No critical or degraded systems detected.
`; return; } alertList.innerHTML = scored.map((sys) => { const cls = sys.score === 3 ? "critical" : sys.score === 2 ? "degraded" : "unknown"; return `
${sys.name} ${sys.severity || sys.status} • ${sys.layer} • Latency: ${sys.latency || "N/A"}
`; }).join(""); }