// Dynamically include fragments async function includeFragment(id, url) { const res = await fetch(url); const html = await res.text(); document.getElementById(id).innerHTML = html; } // Load a markdown file into #page async function loadMarkdown(page) { const mdPath = `/md/${page}.md`; try { const response = await fetch(mdPath); const hasMarkdown = response.ok; const mdText = hasMarkdown ? await response.text() : ""; // Load nav metadata (top + sidepanel) const [navRes, sideNavRes] = await Promise.all([ fetch('/api/nav'), fetch('/api/nav/sidepanel') ]); const navItems = await navRes.json(); const sideNav = await sideNavRes.json(); // ---- Locate metadata (nav.json first, then nav-sidepanel.json) ---- let meta = null; // 1) nav.json — brand meta = navItems.find(item => item.brand === true && item.page === page); // 2) nav.json — top-level if (!meta) { meta = navItems.find(item => item.page === page); } // 3) nav.json — children if (!meta) { for (const item of navItems) { if (Array.isArray(item.children)) { const child = item.children.find(c => c.page === page); if (child) { meta = child; break; } } } } // 4) nav-sidepanel.json — section headers if (!meta) { for (const section of Object.values(sideNav)) { if (section.page === page) { meta = section; break; } } } // 5) nav-sidepanel.json — section items if (!meta) { for (const section of Object.values(sideNav)) { if (Array.isArray(section.items)) { const item = section.items.find(i => i.page === page); if (item) { meta = item; break; } } } } // Fallback if (!meta) { meta = { title: page, icon: "" }; } // Fallback title if still not found if (!meta) { meta = { title: page, icon: "" }; } // ---- Build header HTML (NOT passed to marked) ---- const headerHtml = `

${meta.icon ? `` : ""} ${meta.title}

`; // ---- Render header directly ---- const pageEl = document.getElementById('page'); pageEl.innerHTML = headerHtml; // ---- Render Markdown content below header ---- if (hasMarkdown) { pageEl.innerHTML += marked.parse(mdText); } else { pageEl.innerHTML += marked.parse(` > No markdown file found for: \`${page}.md\` `); } // Load optional page-specific JS await loadPageScript(page); // Re-render MathJax if (window.MathJax?.typesetPromise) { await MathJax.typesetPromise(); } } catch (err) { console.error(err); document.getElementById('page').innerHTML = `
Error loading Markdown: ${err.message}
`; } } // Dynamically load a JS file if it exists async function loadPageScript(page) { const jsPath = `/js/${page}.js`; // ✅ declare properly try { // HEAD request checks whether the file exists without downloading it const res = await fetch(jsPath, { method: 'HEAD' }); if (!res.ok) { console.warn(`Script not found: ${jsPath}`); return; } // Dynamically inject script into the DOM const script = document.createElement('script'); script.type = 'module'; script.src = jsPath; script.defer = true; document.body.appendChild(script); console.log(`Loading script: ${jsPath}`); } catch (err) { console.warn(`Error loading script ${jsPath}: ${err.message}`); } } // Load Nav function loadNav() { fetch('/api/nav') .then(res => res.json()) .then(navItems => { const navContainer = document.getElementById('nav'); if (!navContainer) return; const urlParams = new URLSearchParams(window.location.search); const currentPage = urlParams.get('page') || 'index'; const enabledItems = navItems.filter(item => item.enabled !== false); // --------- FIND BRAND ITEM ---------- const brandItem = enabledItems.find(x => x.brand === true); // --------- CREATE NAV STRUCTURE ---------- const navEl = document.createElement('nav'); navEl.className = 'navbar navbar-expand-lg bg-body-tertiary'; const container = document.createElement('div'); container.className = 'container-fluid'; // BRAND if (brandItem) { const brand = document.createElement('a'); brand.className = 'navbar-brand'; brand.href = `index.html?page=${brandItem.page}`; brand.innerHTML = `${brandItem.title}`; container.appendChild(brand); } // TOGGLER BUTTON const toggler = document.createElement('button'); toggler.className = 'navbar-toggler'; toggler.type = 'button'; toggler.setAttribute('data-bs-toggle', 'collapse'); toggler.setAttribute('data-bs-target', '#navbarNavDropdown'); toggler.setAttribute('aria-controls', 'navbarNavDropdown'); toggler.setAttribute('aria-expanded', 'false'); toggler.setAttribute('aria-label', 'Toggle navigation'); toggler.innerHTML = ''; container.appendChild(toggler); // COLLAPSE REGION const collapse = document.createElement('div'); collapse.className = 'collapse navbar-collapse'; collapse.id = 'navbarNavDropdown'; const ul = document.createElement('ul'); ul.className = 'navbar-nav me-auto mb-2 mb-lg-0'; enabledItems.forEach(item => { // skip the brand from the menu if (item.brand === true) return; const hasChildren = Array.isArray(item.children) && item.children.length > 0; const li = document.createElement('li'); li.className = hasChildren ? 'nav-item dropdown' : 'nav-item'; // --- NO CHILDREN: NORMAL NAV LINK --- if (!hasChildren) { const a = document.createElement('a'); let cls = 'nav-link'; if (item.page === currentPage) cls += ' active'; a.className = cls; a.href = `index.html?page=${item.page}`; a.innerHTML = `${item.title}`; li.appendChild(a); ul.appendChild(li); return; } // --- HAS CHILDREN: DROPDOWN TOGGLE --- const toggle = document.createElement('a'); toggle.className = 'nav-link dropdown-toggle'; toggle.href = '#'; toggle.role = 'button'; toggle.setAttribute('data-bs-toggle', 'dropdown'); toggle.setAttribute('aria-expanded', 'false'); toggle.innerHTML = `${item.title}`; const menu = document.createElement('ul'); menu.className = 'dropdown-menu'; item.children .filter(child => child.enabled !== false) .forEach(child => { const childLi = document.createElement('li'); const childA = document.createElement('a'); let childClass = 'dropdown-item'; if (child.page === currentPage) { childClass += ' active'; toggle.classList.add('active'); // highlight parent } childA.className = childClass; childA.href = `index.html?page=${child.page}`; childA.innerHTML = `${child.icon ? `` : ''}${child.title}`; childLi.appendChild(childA); menu.appendChild(childLi); }); li.appendChild(toggle); li.appendChild(menu); ul.appendChild(li); }); collapse.appendChild(ul); container.appendChild(collapse); navEl.appendChild(container); navContainer.innerHTML = ''; navContainer.appendChild(navEl); // --------- MANUAL DROPDOWN HANDLERS --------- const dropdownToggles = navContainer.querySelectorAll('.nav-item.dropdown > .dropdown-toggle'); dropdownToggles.forEach(toggle => { toggle.addEventListener('click', evt => { evt.preventDefault(); const parentLi = toggle.parentElement; const menu = parentLi.querySelector('.dropdown-menu'); const isShown = menu.classList.contains('show'); // close all others navContainer.querySelectorAll('.dropdown-menu.show').forEach(openMenu => { openMenu.classList.remove('show'); }); // toggle current if (!isShown) { menu.classList.add('show'); } }); }); // close dropdown when clicking outside nav document.addEventListener('click', evt => { if (!navContainer.contains(evt.target)) { navContainer.querySelectorAll('.dropdown-menu.show').forEach(openMenu => { openMenu.classList.remove('show'); }); } }); }) .catch(err => { console.error('Failed to load nav:', err); const navContainer = document.getElementById('nav'); if (navContainer) { navContainer.innerHTML = '

Failed to load navigation menu.

'; } }); } function loadNavTabs() { fetch('/api/nav') .then(res => res.json()) .then(navItems => { const ul = document.createElement('ul'); ul.className = 'nav nav-tabs'; const urlParams = new URLSearchParams(window.location.search); const currentPage = urlParams.get('page') || 'index'; // Filter out disabled items const enabledItems = navItems.filter(item => item.enabled !== false); enabledItems.forEach(item => { const li = document.createElement('li'); li.className = 'nav-item'; const a = document.createElement('a'); a.className = 'nav-link'; a.href = `index.html?page=${item.page}`; a.innerHTML = `${item.title}`; // Apply 'active' class to the current tab if (currentPage === item.page) { a.classList.add('active'); a.setAttribute('aria-current', 'page'); } // Optional: Support for disabled nav items in nav.json if (item.disabled) { a.classList.add('disabled'); a.setAttribute('aria-disabled', 'true'); } li.appendChild(a); ul.appendChild(li); }); const navContainer = document.getElementById('nav'); if (navContainer) { navContainer.innerHTML = ''; // Clear previous content navContainer.appendChild(ul); } }) .catch(err => { console.error("Failed to load nav:", err); document.getElementById('nav').innerHTML = '

Failed to load navigation menu.

'; }); } // =============================== // Side Panel (layout-collapsing) // =============================== async function loadSidePanel() { // console.log('DEBUG: loadSidePanel() called'); const panel = document.getElementById('sidepanel'); const inner = document.getElementById('sidepanel-inner'); const toggleBtn = document.getElementById('sidepanel-toggle'); if (!panel || !inner || !toggleBtn) return; const currentPage = new URLSearchParams(window.location.search).get('page') || 'index'; const isCollapsed = localStorage.getItem('sidepanel_collapsed') === 'true'; if (isCollapsed) { panel.classList.add('collapsed'); toggleBtn.innerHTML = ``; } else { toggleBtn.innerHTML = ``; } try { const res = await fetch('/api/nav/sidepanel'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const sideNav = await res.json(); renderAllSidePanelSections(inner, sideNav, currentPage); } catch (err) { console.error('Failed to load side panel:', err); inner.innerHTML = ''; } // Collapse / expand behavior toggleBtn.onclick = () => { const collapsed = panel.classList.toggle('collapsed'); localStorage.setItem('sidepanel_collapsed', collapsed); toggleBtn.innerHTML = ` `; }; } // ------------------------------- // Find matching section by page // ------------------------------- function findSidePanelSection(sideNav, page) { for (const key in sideNav) { const section = sideNav[key]; if (!section || section.enabled === false) continue; if (Array.isArray(section.items)) { const hit = section.items.find( item => item.enabled !== false && item.page === page ); if (hit) return section; } } return null; } // ------------------------------- // Render side panel content // ------------------------------- function renderAllSidePanelSections(container, sideNav, currentPage) { container.innerHTML = ''; Object.values(sideNav) .filter(section => section.enabled !== false) .forEach(section => { // ---- Section Header (linkable if page exists) ---- const header = document.createElement( section.page ? 'a' : 'div' ); if (section.desc) { header.title = section.desc; } header.className = 'fw-bold px-3 pt-3 pb-2 small nav-link'; // 'fw-bold px-3 pt-3 pb-2 text-uppercase small nav-link'; if (section.page) { header.href = `index.html?page=${section.page}`; if (section.page === currentPage) { header.classList.add('active'); } } header.innerHTML = ` ${section.icon ? `` : ''} ${section.title} `; container.appendChild(header); // ---- Section Items (if any) ---- if (Array.isArray(section.items) && section.items.length > 0) { const ul = document.createElement('ul'); ul.className = 'nav flex-column px-2'; section.items .filter(item => item.enabled !== false) .forEach(item => { const li = document.createElement('li'); li.className = 'nav-item'; const a = document.createElement('a'); a.className = 'nav-link py-2'; if (item.page === currentPage) { a.classList.add('active'); } if (item.desc) { a.title = item.desc; } a.href = `index.html?page=${item.page}`; a.innerHTML = ` ${item.icon ? `` : ''} ${item.title} `; li.appendChild(a); ul.appendChild(li); }); container.appendChild(ul); } }); } // Load Footer function loadFooter() { // Load API Config JSON from /api/config fetch('/api/config') .then(res => res.json()) .then(config => { let html = ''; html += '\n'; // document.getElementById('app-description').innerHTML = config.package.description; document.getElementById('footer').innerHTML = html; }) .catch(err => { console.error('Error loading config:', err); document.getElementById('footer').innerHTML = ''; }); } export function genBootStrapTable(json_obj, tblsize='1') { // json_obj structure // [ // [ // "value1", // "value2", // "value3" // ], // [ // "value1", // "value2", // "value3" // ] // ] // console.log("json_obj: ", JSON.stringify(json_obj, null, 2)); // console.log("tblsize: ", tblsize); // A basic table with rows of content and no headers. let html = ''; html += '\n'; html += '\n'; for (let row in json_obj) { html += `\n`; for (let col in json_obj[row]) { html += `\n`; } html += `\n`; } html += '\n'; html += '
${json_obj[row][col]}
\n'; return html; } // SQL JSON To Bootstrap Table export function sqlJsonToBootstrapTable(json_obj, col_disp_map) { // console.log("Running sqlJsonToBootstrapTable"); // console.log("json_obj: ", JSON.stringify(json_obj, null, 2)); let table_html = ''; if (json_obj.length == 0) { table_html += "

No results found

\n"; return table_html; } else { table_html += "\n"; table_html += "\n"; table_html += "\n"; // Get column names let cols = []; for (let col in json_obj[0]) { cols.push(col); } // console.log("cols: ", JSON.stringify(cols, null, 2)); // console.log("col_disp_map: ", JSON.stringify(col_disp_map, null, 2)); // Add column names to table for (let col in cols) { // table_html += `\n`; // console.log(`cols[col]: ${cols[col]}`); table_html += `\n`; } table_html += "\n"; table_html += "\n"; for (let row in json_obj) { table_html += "\n"; for (let col in cols) { switch (cols[col]) { case 'id': table_html += ` \n`; break; case 'balance': { const val = Number(json_obj[row][cols[col]]); if (!isNaN(val)) { const formatted = val.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); table_html += `\n`; } else { table_html += `\n`; } break; } case 'trans_amount': { const val = Number(json_obj[row][cols[col]]); if (!isNaN(val)) { const formatted = val.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); table_html += `\n`; } else { table_html += `\n`; } break; } case 'trans_id': // Fixed width, monospace for alignment table_html += `\n`; // table_html += `\n`; break; case 'trans_date': // Fixed width, monospace for alignment table_html += `\n`; // table_html += `\n`; break; case 'notes': table_html += `\n`; break; default: table_html += `\n`; break; } // table_html += `\n`; } table_html += "\n"; } table_html += "\n"; table_html += "
${cols[col]}${col_disp_map[cols[col]]}
${json_obj[row][cols[col]]} ${formatted}${json_obj[row][cols[col]]}${formatted}${json_obj[row][cols[col]]}${json_obj[row][cols[col]]}${json_obj[row][cols[col]]}${json_obj[row][cols[col]]}${json_obj[row][cols[col]]}${json_obj[row][cols[col]]}${json_obj[row][cols[col]]}${json_obj[row][cols[col]]}
\n"; // console.log("Table HTML: "); // console.log(table_html); return table_html; } } // Load config (always from API) export async function loadConfig() { const res = await fetch('/api/config', { cache: 'no-store' }); return await res.json(); } // Load config // export async function loadConfig() { // const cached = sessionStorage.getItem('ssl_config'); // if (cached) { // // console.log('Loaded config from cache'); // return JSON.parse(cached); // } // const res = await fetch('/api/config'); // const config = await res.json(); // sessionStorage.setItem('ssl_config', JSON.stringify(config)); // // console.log('Fetched config from backend'); // return config; // } export async function getConfig() { const res = await fetch('/api/config', { cache: 'no-store' }); return await res.json(); } // export async function getConfig() { // const cached = sessionStorage.getItem('ssl_config'); // if (!cached) { // throw new Error('ssl_config not found in sessionStorage'); // } // return JSON.parse(cached); // } // // Load tags // export async function loadTags(forceRefresh = false) { // if (!forceRefresh) { // const cached = localStorage.getItem('app_tags'); // if (cached) { // // console.log('Loaded tags from cache'); // return JSON.parse(cached); // } // } // const res = await fetch('/api/sql/query/tags'); // ✅ wait for fetch to resolve // const tags = await res.json(); // ✅ wait for JSON parsing // localStorage.setItem('app_tags', JSON.stringify(tags)); // // console.log('Fetched tags from backend'); // return tags; // } // export async function refreshTags() { // const tags = await loadTags(true); // force refresh // // console.log('Tags reloaded:', tags); // } // export async function tag_badge(tag, active = true) { // const tags = JSON.parse(localStorage.getItem('app_tags')) || []; // const t = tags.find(x => x.name === tag); // if (!t) return `${tag}`; // const safeName = t.name.replace(/\W+/g, '_'); // const styleActive = `background-color:${t.color};border-color:${t.color};color:#fff;`; // const styleInactive = `color:${t.color};background-color:#e9ecef;border:1px solid ${t.color};`; // const style = active ? styleActive : styleInactive; // return ` // ${t.name} // `; // } export async function getLocalTimestamp() { const now = new Date(); const pad = n => String(n).padStart(2, '0'); const offset = -now.getTimezoneOffset(); const sign = offset >= 0 ? '+' : '-'; const offsetHours = pad(Math.floor(Math.abs(offset) / 60)); const offsetMinutes = pad(Math.abs(offset) % 60); return `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${sign}${offsetHours}:${offsetMinutes}`; } export async function getUTCTimestamp() { const now = new Date(); return `${now.getUTCFullYear()}-${pad(now.getUTCMonth()+1)}-${pad(now.getUTCDate())}T${pad(now.getUTCHours())}:${pad(now.getUTCMinutes())}:${pad(now.getUTCSeconds())}Z`; } // =============================== // Load on page load // =============================== window.addEventListener('DOMContentLoaded', async () => { const config = await loadConfig(); // console.log(`DEBUG: ${JSON.stringify(config)}`) // const tags = await loadTags(); // console.log(`DEBUG: ${JSON.stringify(tags)}`) // Load fragments and layout includeFragment('banner', '/banner.html'); // includeFragment('footer', '/footer.html'); loadNav(); loadSidePanel(); loadFooter(); // Load Markdown content const urlParams = new URLSearchParams(window.location.search); const page = urlParams.get('page') || 'index'; loadMarkdown(`${page}`); // Update title and app description if (config?.package?.description) { document.title = config.package.description; const descEl = document.querySelector('.app-description'); if (descEl) descEl.innerHTML = config.package.description; } });