mirror of
https://github.com/ced4568/ced-portfolio.git
synced 2026-08-13 06:44:02 +00:00
Major portfolio overhaul: redesign, new sections, LinkedIn/GitHub badges, resume download, GitHub repos, tech stack, NOC improvements
- Full visual redesign with JetBrains Mono + Syne + DM Sans fonts, improved terminal card, blinking cursor, and eyebrow dot animation - Added LinkedIn and GitHub badge pills in hero with correct LinkedIn URL (chase-dumphord) - Added Tech Stack section with 6 groups including AI & Automation - Added GitHub Repos section populated live from GitHub API - Added resume PDF download button pointing to assets/ChaseD_Resume.pdf - NOC: added fallback block, live/error dot states, auto-refresh badge, View Live NOC CTA on roadmap - Updated About section with AI/LLM integration mention and new Core Focus Areas bullet - Added sitemap.xml with main site and NOC subdomain entries - Updated noc-status.json with cleaner data structure and 8 real systems - Added Open Graph meta tags, favicon links, rel=me for LinkedIn and GitHub, robots meta Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
@@ -1,148 +1,273 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const yearSpan = document.querySelector("[data-year]");
|
||||
if (yearSpan) {
|
||||
yearSpan.textContent = new Date().getFullYear();
|
||||
}
|
||||
// ═══════════════════════════════════════════════
|
||||
// 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();
|
||||
});
|
||||
|
||||
async function loadNocStatus() {
|
||||
try {
|
||||
const response = await fetch("data/noc-status.json");
|
||||
// Track consecutive failures so we can show the fallback
|
||||
let failCount = 0;
|
||||
const MAX_FAILS = 3;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error: ${response.status}`);
|
||||
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`;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Hide fallback if it was showing
|
||||
if (fallback) fallback.style.display = "none";
|
||||
|
||||
const statusText = document.getElementById("noc-status-text");
|
||||
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");
|
||||
// ── 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);
|
||||
|
||||
if (!statusText || !nodes || !services || !vlans || !edge || !grid) {
|
||||
// ── 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 = `
|
||||
<div class="noc-system-top">
|
||||
<span class="mini-status ${statusClass}"></span>
|
||||
<span>${system.layer}</span>
|
||||
</div>
|
||||
<div class="system-label">${system.label || "System"}</div>
|
||||
<div class="severity-pill ${(system.severity || "unknown").toLowerCase()}">
|
||||
${system.severity || "Unknown"}
|
||||
</div>
|
||||
<h3>${system.name}</h3>
|
||||
<p>${system.type}</p>
|
||||
<code>${system.address}</code>
|
||||
<div class="system-meta">
|
||||
<small>Status: ${system.status}</small>
|
||||
<small>Latency: ${system.latency || "N/A"}</small>
|
||||
<small>Last Check: ${system.lastCheck || "Manual"}</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `<p class="noc-loading">No public repositories found.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
statusText.textContent = `${data.nocName} — ${data.status} — Last updated: ${data.lastUpdated}`;
|
||||
container.innerHTML = ownRepos.map((repo) => {
|
||||
const langColor = LANG_COLORS[repo.language] || "var(--accent-2)";
|
||||
const desc = repo.description
|
||||
? repo.description
|
||||
: "No description provided.";
|
||||
|
||||
animateMetric(nodes, data.summary.nodes);
|
||||
animateMetric(services, data.summary.services);
|
||||
animateMetric(vlans, data.summary.vlans);
|
||||
animateMetric(edge, data.summary.edgeSystems);
|
||||
const updatedDate = new Date(repo.pushed_at).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
grid.innerHTML = "";
|
||||
return `
|
||||
<div class="repo-card">
|
||||
<div class="repo-card-top">
|
||||
<span class="repo-icon">📁</span>
|
||||
<a
|
||||
class="repo-name"
|
||||
href="${repo.html_url}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="${repo.name}"
|
||||
>${repo.name}</a>
|
||||
<span class="repo-visibility">${repo.private ? "private" : "public"}</span>
|
||||
</div>
|
||||
|
||||
data.systems.forEach((system) => {
|
||||
const card = document.createElement("article");
|
||||
card.className = "noc-system-card";
|
||||
<p class="repo-desc">${desc}</p>
|
||||
|
||||
const status = system.status.toLowerCase();
|
||||
|
||||
const statusClass =
|
||||
status.includes("online") || status.includes("operational")
|
||||
? "online"
|
||||
: status.includes("building") || status.includes("tuning") || status.includes("active")
|
||||
? "warning"
|
||||
: "offline";
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="noc-system-top">
|
||||
<span class="mini-status ${statusClass}"></span>
|
||||
<span>${system.layer}</span>
|
||||
</div>
|
||||
|
||||
<div class="system-label">${system.label || "System"}</div>
|
||||
|
||||
<div class="severity-pill ${system.severity?.toLowerCase() || "unknown"}">
|
||||
${system.severity || "Unknown"}
|
||||
</div>
|
||||
|
||||
<h3>${system.name}</h3>
|
||||
<p>${system.type}</p>
|
||||
<code>${system.address}</code>
|
||||
|
||||
<div class="system-meta">
|
||||
<small>Status: ${system.status}</small>
|
||||
<small>Latency: ${system.latency || "N/A"}</small>
|
||||
<small>Last Check: ${system.lastCheck || "Manual"}</small>
|
||||
<div class="repo-meta">
|
||||
${repo.language
|
||||
? `<span>
|
||||
<span class="repo-lang-dot" style="background:${langColor}"></span>
|
||||
${repo.language}
|
||||
</span>`
|
||||
: ""}
|
||||
<span>★ ${repo.stargazers_count}</span>
|
||||
<span>Updated ${updatedDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
if (alertList) {
|
||||
renderAlerts(data.systems, alertList);
|
||||
} catch (err) {
|
||||
console.warn("[GitHub] Failed to load repos:", err.message);
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="repo-loading">
|
||||
<p class="noc-loading">
|
||||
Could not load repositories.
|
||||
<a href="https://github.com/ced4568" target="_blank" rel="noopener noreferrer">
|
||||
View on GitHub →
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
const statusText = document.getElementById("noc-status-text");
|
||||
|
||||
if (statusText) {
|
||||
statusText.textContent = "Ced’s NOC data unavailable";
|
||||
}
|
||||
|
||||
console.error("Failed to load NOC status:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function animateMetric(element, value) {
|
||||
element.classList.remove("metric-pop");
|
||||
element.textContent = value;
|
||||
|
||||
void element.offsetWidth;
|
||||
element.classList.add("metric-pop");
|
||||
}
|
||||
|
||||
function renderAlerts(systems, alertList) {
|
||||
const priority = systems
|
||||
.map((system) => {
|
||||
const severity = (system.severity || "Unknown").toLowerCase();
|
||||
const status = (system.status || "").toLowerCase();
|
||||
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;
|
||||
|
||||
if (severity.includes("critical") || status.includes("offline")) {
|
||||
score = 3;
|
||||
} else if (severity.includes("degraded")) {
|
||||
score = 2;
|
||||
} else if (severity.includes("unknown")) {
|
||||
score = 1;
|
||||
}
|
||||
|
||||
return { ...system, score };
|
||||
return { ...sys, score };
|
||||
})
|
||||
.filter((system) => system.score > 0)
|
||||
.filter((s) => s.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 3);
|
||||
|
||||
if (priority.length === 0) {
|
||||
if (scored.length === 0) {
|
||||
alertList.innerHTML = `
|
||||
<div class="noc-alert healthy">
|
||||
<strong>All monitored systems healthy</strong>
|
||||
<strong>All monitored systems nominal</strong>
|
||||
<span>No critical or degraded systems detected.</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
alertList.innerHTML = priority
|
||||
.map((system) => {
|
||||
const alertClass =
|
||||
system.score === 3 ? "critical" : system.score === 2 ? "degraded" : "unknown";
|
||||
alertList.innerHTML = scored.map((sys) => {
|
||||
const cls =
|
||||
sys.score === 3 ? "critical"
|
||||
: sys.score === 2 ? "degraded"
|
||||
: "unknown";
|
||||
|
||||
return `
|
||||
<div class="noc-alert ${alertClass}">
|
||||
<strong>${system.name}</strong>
|
||||
<span>${system.severity || system.status} • ${system.layer} • ${system.latency || "N/A"}</span>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
return `
|
||||
<div class="noc-alert ${cls}">
|
||||
<strong>${sys.name}</strong>
|
||||
<span>${sys.severity || sys.status} • ${sys.layer} • Latency: ${sys.latency || "N/A"}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user