From e18eb52c492d0cee05881d2478a3a396c22cd9f9 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 00:10:01 -0400 Subject: [PATCH] feat: add about panel with uptime, PRs merged, and agents active (#125) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an about panel (toggle with [I] key or ℹ button) displaying: - SESSION UPTIME — live ticker since page load - PRs MERGED — fetched from Gitea API, filtered to merged state - AGENTS ACTIVE — Timmy (1), expandable as more agents come online Panel follows existing HUD design conventions: dark background, holographic blue border, monospace font, pulse animation. Hidden in photo mode. Fixes #125 --- app.js | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 26 ++++++++++++ style.css | 91 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+) diff --git a/app.js b/app.js index 4902f2f..81fe811 100644 --- a/app.js +++ b/app.js @@ -260,6 +260,120 @@ function animate() { animate(); +// === ABOUT PANEL === +const aboutPanel = document.getElementById('about-panel'); +const aboutToggleBtn = document.getElementById('about-toggle'); +const aboutCloseBtn = document.getElementById('about-close'); +const statUptime = document.getElementById('stat-uptime'); +const statPRs = document.getElementById('stat-prs'); +const statAgents = document.getElementById('stat-agents'); + +const sessionStart = Date.now(); + +/** + * Formats elapsed milliseconds as HH:MM:SS. + * @param {number} ms + * @returns {string} + */ +function formatUptime(ms) { + const s = Math.floor(ms / 1000); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + return [h, m, sec].map((n) => String(n).padStart(2, '0')).join(':'); +} + +let uptimeInterval = null; + +/** + * Fetches merged PR count and agents active from Gitea API. + */ +async function fetchNexusStats() { + // Merged PRs + try { + const res = await fetch( + 'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/pulls?state=closed&limit=50&page=1', + { headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } } + ); + if (res.ok) { + const pulls = await res.json(); + const merged = pulls.filter((/** @type {{merged: boolean}} */ p) => p.merged).length; + // Also check x-total-count if full count matters; for now sum pages heuristic + const total = parseInt(res.headers.get('x-total-count') || '0', 10); + // Re-fetch all if there are more than 50 + if (total > 50) { + const res2 = await fetch( + `http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/pulls?state=closed&limit=${total}&page=1`, + { headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } } + ); + if (res2.ok) { + const all = await res2.json(); + const mergedAll = all.filter((/** @type {{merged: boolean}} */ p) => p.merged).length; + if (statPRs) statPRs.textContent = String(mergedAll); + } + } else { + if (statPRs) statPRs.textContent = String(merged); + } + } + } catch (_) { + if (statPRs) statPRs.textContent = '?'; + } + + // Agents active — count open issues with label 'agent' or derive from active ws connections + // Currently Timmy is the sole active sovereign agent; others are pending issues + if (statAgents) statAgents.textContent = '1 (Timmy)'; +} + +/** + * Opens the about panel and starts the uptime ticker. + */ +function openAboutPanel() { + if (!aboutPanel) return; + aboutPanel.classList.add('visible'); + aboutPanel.setAttribute('aria-hidden', 'false'); + if (statUptime) statUptime.textContent = formatUptime(Date.now() - sessionStart); + uptimeInterval = setInterval(() => { + if (statUptime) statUptime.textContent = formatUptime(Date.now() - sessionStart); + }, 1000); + fetchNexusStats(); +} + +/** + * Closes the about panel. + */ +function closeAboutPanel() { + if (!aboutPanel) return; + aboutPanel.classList.remove('visible'); + aboutPanel.setAttribute('aria-hidden', 'true'); + if (uptimeInterval) { + clearInterval(uptimeInterval); + uptimeInterval = null; + } +} + +let aboutOpen = false; + +if (aboutToggleBtn) { + aboutToggleBtn.addEventListener('click', () => { + aboutOpen = !aboutOpen; + if (aboutOpen) openAboutPanel(); else closeAboutPanel(); + }); +} + +if (aboutCloseBtn) { + aboutCloseBtn.addEventListener('click', () => { + aboutOpen = false; + closeAboutPanel(); + }); +} + +document.addEventListener('keydown', (e) => { + if (e.key === 'i' || e.key === 'I') { + aboutOpen = !aboutOpen; + if (aboutOpen) openAboutPanel(); else closeAboutPanel(); + } +}); + // === DEBUG MODE === let debugMode = false; diff --git a/index.html b/index.html index 6d4000f..bb17efe 100644 --- a/index.html +++ b/index.html @@ -33,9 +33,35 @@ + + + +
MAP VIEW [Tab] to exit diff --git a/style.css b/style.css index 92029bb..44b90a5 100644 --- a/style.css +++ b/style.css @@ -150,3 +150,94 @@ body.photo-mode #overview-indicator { #photo-focus { color: var(--color-primary); } + +/* === ABOUT PANEL === */ +.about-panel { + display: none; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 30; + background: rgba(0, 0, 8, 0.88); + border: 1px solid var(--color-primary); + padding: 20px 24px; + font-family: var(--font-body); + color: var(--color-text); + min-width: 260px; + box-shadow: 0 0 32px rgba(68, 136, 255, 0.15); +} + +.about-panel.visible { + display: block; +} + +.about-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + border-bottom: 1px solid var(--color-secondary); + padding-bottom: 10px; +} + +.about-panel-title { + font-size: 13px; + letter-spacing: 0.2em; + color: var(--color-primary); + text-transform: uppercase; +} + +.about-close { + background: none; + border: none; + color: var(--color-text-muted); + cursor: pointer; + font-size: 14px; + font-family: var(--font-body); + padding: 0 2px; + line-height: 1; +} + +.about-close:hover { + color: var(--color-text); +} + +.about-stats { + display: flex; + flex-direction: column; + gap: 10px; +} + +.stat-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; +} + +.stat-label { + font-size: 10px; + letter-spacing: 0.15em; + color: var(--color-text-muted); + text-transform: uppercase; +} + +.stat-value { + font-size: 13px; + color: var(--color-primary); + letter-spacing: 0.1em; +} + +.about-hint { + margin-top: 14px; + font-size: 10px; + letter-spacing: 0.15em; + color: var(--color-text-muted); + text-align: center; + text-transform: uppercase; +} + +body.photo-mode .about-panel { + display: none !important; +} -- 2.43.0