Files
jontsai_command-center/public/js/sidebar.js

358 lines
9.9 KiB
JavaScript

/**
* Shared Sidebar Loader
*
* Loads the sidebar partial and connects to SSE for live badge updates.
* Include this script in any page that needs the sidebar.
*/
(function () {
"use strict";
// State for sidebar badges
const sidebarState = {
sessions: 0,
cron: 0,
jobs: 0,
memory: 0,
cerebro: 0,
operators: 0,
tokens: "-",
cost: "-",
monthlyCost: "-",
avgTokens: "-",
avgCost: "-",
lastUpdated: null,
};
// SSE connection
let eventSource = null;
let reconnectAttempts = 0;
const MAX_RECONNECT_DELAY = 30000;
/**
* Load and inject sidebar HTML
*/
async function loadSidebar() {
try {
const response = await fetch("/partials/sidebar.html");
if (!response.ok) throw new Error("Failed to load sidebar");
const html = await response.text();
// Find or create sidebar container
let container = document.getElementById("sidebar-container");
if (!container) {
// Insert at start of body
container = document.createElement("div");
container.id = "sidebar-container";
document.body.insertBefore(container, document.body.firstChild);
}
container.innerHTML = html;
if (window.I18N?.translateSubtree) {
window.I18N.translateSubtree(container);
}
// Set active state based on current page
setActiveNavItem();
// Connect to SSE for live updates
connectSSE();
// Also fetch initial state
fetchSidebarState();
} catch (error) {
console.error("[Sidebar] Failed to load:", error);
}
}
/**
* Check if we're on the main page
*/
function isMainPage() {
const path = window.location.pathname;
return path === "/" || path === "/index.html";
}
/**
* Set the active nav item based on current URL
*/
function setActiveNavItem() {
const currentPath = window.location.pathname;
const currentHash = window.location.hash;
document.querySelectorAll(".nav-item").forEach((item) => {
item.classList.remove("active");
const itemPage = item.dataset.page;
const itemHref = item.getAttribute("href");
// Check if this nav item matches the current page
if (itemPage === "/" && isMainPage()) {
// For main page sections
if (currentHash && itemHref && itemHref === currentHash) {
item.classList.add("active");
} else if (!currentHash && item.dataset.section === "vitals") {
// Default to vitals on main page with no hash
item.classList.add("active");
}
} else if (itemHref === currentPath) {
// Exact page match (like /jobs.html)
item.classList.add("active");
}
});
}
/**
* Set up navigation click handlers
* - Hash links on main page: smooth scroll
* - Hash links on other pages: navigate to main page with hash
*/
function setupNavigation() {
document.querySelectorAll(".nav-item[data-section]").forEach((item) => {
item.addEventListener("click", (e) => {
const section = item.dataset.section;
const targetHash = `#${section}-section`;
if (isMainPage()) {
// On main page: smooth scroll to section
e.preventDefault();
const target = document.querySelector(targetHash);
if (target) {
target.scrollIntoView({ behavior: "smooth" });
history.pushState(null, "", targetHash);
setActiveNavItem();
}
} else {
// On other page: navigate to main page with hash
e.preventDefault();
window.location.href = "/" + targetHash;
}
});
});
}
/**
* Connect to SSE for live updates
*/
function connectSSE() {
if (typeof EventSource === "undefined") {
console.warn("[Sidebar SSE] Not supported");
return;
}
eventSource = new EventSource("/api/events");
eventSource.onopen = () => {
console.log("[Sidebar SSE] Connected");
reconnectAttempts = 0;
};
eventSource.addEventListener("update", (e) => {
try {
const data = JSON.parse(e.data);
handleStateUpdate(data);
} catch (err) {
console.error("[Sidebar SSE] Parse error:", err);
}
});
eventSource.addEventListener("heartbeat", () => {
sidebarState.lastUpdated = new Date();
updateTimestamp();
});
eventSource.onerror = () => {
console.error("[Sidebar SSE] Connection error");
eventSource.close();
// Exponential backoff reconnect
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts - 1), MAX_RECONNECT_DELAY);
setTimeout(connectSSE, delay);
};
}
/**
* Fetch initial sidebar state
*/
async function fetchSidebarState() {
try {
const response = await fetch("/api/state");
const data = await response.json();
handleStateUpdate(data);
} catch (error) {
console.error("[Sidebar] Failed to fetch state:", error);
}
}
/**
* Handle state updates and update badges
*/
function handleStateUpdate(data) {
// Update session count
if (data.sessions) {
sidebarState.sessions = data.sessions.length || 0;
}
if (data.statusCounts) {
sidebarState.sessions = data.statusCounts.all || 0;
}
// Update cron count
if (data.cron) {
sidebarState.cron = Array.isArray(data.cron) ? data.cron.length : 0;
}
// Update jobs count (from jobs API if available)
if (data.jobs) {
sidebarState.jobs = Array.isArray(data.jobs) ? data.jobs.length : data.jobs.total || 0;
}
// Update memory count
if (data.memory) {
sidebarState.memory = data.memory.fileCount || data.memory.totalFiles || 0;
}
// Update cerebro count
if (data.cerebro) {
sidebarState.cerebro = data.cerebro.topicCount || data.cerebro.totalTopics || 0;
}
// Update operators count
if (data.operators) {
sidebarState.operators = Array.isArray(data.operators.operators)
? data.operators.operators.length
: 0;
}
// Update token stats
if (data.tokenStats) {
sidebarState.tokens = data.tokenStats.totalFormatted || data.tokenStats.total || "-";
sidebarState.cost = data.tokenStats.estCostFormatted || data.tokenStats.estCost || "-";
sidebarState.monthlyCost =
data.tokenStats.estMonthlyCostFormatted || data.tokenStats.estMonthlyCost || "-";
sidebarState.avgTokens = data.tokenStats.avgTokensPerSession || "-";
sidebarState.avgCost = data.tokenStats.avgCostPerSession || "-";
}
sidebarState.lastUpdated = new Date();
// Update the DOM
updateBadges();
updateTimestamp();
}
/**
* Update badge elements
*/
function updateBadges() {
const updates = {
"nav-session-count": sidebarState.sessions,
"nav-cron-count": sidebarState.cron,
"nav-jobs-count": sidebarState.jobs || "-",
"nav-memory-count": sidebarState.memory,
"nav-cerebro-count": sidebarState.cerebro,
"nav-operator-count": sidebarState.operators,
"nav-tokens": sidebarState.tokens,
"nav-cost": sidebarState.cost,
"nav-monthly-cost": sidebarState.monthlyCost,
"nav-avg-tokens": sidebarState.avgTokens,
"nav-avg-cost": sidebarState.avgCost,
};
for (const [id, value] of Object.entries(updates)) {
const el = document.getElementById(id);
if (el && el.textContent !== String(value)) {
el.textContent = value;
}
}
}
/**
* Update the timestamp in sidebar footer
*/
function updateTimestamp() {
const el = document.getElementById("sidebar-updated");
if (el && sidebarState.lastUpdated) {
const timeStr = sidebarState.lastUpdated.toLocaleTimeString();
const t = window.I18N?.t;
el.textContent = t ? t("sidebar.live", { time: timeStr }) : `Live: ${timeStr}`;
}
}
/**
* Toggle sidebar collapsed state
*/
window.toggleSidebar = function () {
const sidebar = document.getElementById("sidebar");
const mainWrapper = document.getElementById("main-wrapper");
if (sidebar) {
sidebar.classList.toggle("collapsed");
}
if (mainWrapper) {
mainWrapper.classList.toggle("sidebar-collapsed");
}
// Save preference
const collapsed = sidebar?.classList.contains("collapsed");
try {
localStorage.setItem("sidebar-collapsed", collapsed ? "true" : "false");
} catch (e) {}
};
/**
* Restore sidebar collapsed state from localStorage
*/
function restoreSidebarState() {
try {
const collapsed = localStorage.getItem("sidebar-collapsed") === "true";
if (collapsed) {
const sidebar = document.getElementById("sidebar");
const mainWrapper = document.getElementById("main-wrapper");
if (sidebar) sidebar.classList.add("collapsed");
if (mainWrapper) mainWrapper.classList.add("sidebar-collapsed");
}
} catch (e) {}
}
// Fetch jobs count separately (since it's a different API)
async function fetchJobsCount() {
try {
const response = await fetch("/api/jobs");
const data = await response.json();
sidebarState.jobs = data.jobs?.length || 0;
updateBadges();
} catch (error) {
// Jobs API may not be available
}
}
// Initialize on DOM ready
function init() {
loadSidebar().then(() => {
restoreSidebarState();
setupNavigation();
fetchJobsCount();
});
// Listen for hash changes to update active state
window.addEventListener("hashchange", setActiveNavItem);
window.addEventListener("i18n:updated", () => {
const container = document.getElementById("sidebar-container");
if (container && window.I18N?.translateSubtree) {
window.I18N.translateSubtree(container);
}
updateTimestamp();
setActiveNavItem();
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();