/* Sage — knowledge base app */ let allArticles = []; let navIndex = null; let currentArticleId = null; async function loadData() { const [indexRes, ...articleRes] = await Promise.all([ fetch('/sage/api/_index.json'), ...ARTICLE_IDS.map(id => fetch(`/sage/api/${id}.json`)) ]); navIndex = await indexRes.json(); allArticles = await Promise.all(articleRes.map(r => r.json())); } const ARTICLE_IDS = [ 'ssh-keys', 'ssh-access-controls', 'nginx-config', 'disk-logs', 'file-permissions', 'cron-jobs', 'time-sync', 'package-management' ]; const CATEGORY_LABELS = { access: 'Access & Authentication', web: 'Web Services', storage: 'Storage & Logs', sysadmin: 'System Administration', packages: 'Package Management' }; // ── Sidebar ─────────────────────────────────────────────────────────────────── function buildNav() { const sidebar = document.getElementById('sidebar'); sidebar.innerHTML = ''; // Home link const homeWrap = document.createElement('div'); homeWrap.className = 'nav-section'; const homeLink = document.createElement('a'); homeLink.className = 'nav-link'; homeLink.textContent = '⌂ Home'; homeLink.dataset.home = '1'; homeLink.onclick = (e) => { e.preventDefault(); showHome(); }; homeWrap.appendChild(homeLink); sidebar.appendChild(homeWrap); navIndex.categories.forEach(cat => { const section = document.createElement('div'); section.className = 'nav-section'; const label = document.createElement('div'); label.className = 'nav-category'; label.textContent = cat.label; section.appendChild(label); cat.articles.forEach(id => { const article = allArticles.find(a => a.id === id); if (!article) return; const link = document.createElement('a'); link.className = 'nav-link'; link.textContent = article.title; link.dataset.articleId = id; link.onclick = (e) => { e.preventDefault(); showArticle(id); }; section.appendChild(link); }); sidebar.appendChild(section); }); } function setActiveNav(articleId) { document.querySelectorAll('.nav-link').forEach(el => { el.classList.toggle('active', articleId ? el.dataset.articleId === articleId : !!el.dataset.home ); }); } // ── Home ────────────────────────────────────────────────────────────────────── function showHome() { currentArticleId = null; clearSearch(); setActiveNav(null); const main = document.getElementById('main'); main.classList.remove('hidden'); document.getElementById('search-results').classList.remove('visible'); main.innerHTML = ''; const home = document.createElement('div'); home.id = 'home-page'; const h1 = document.createElement('h1'); h1.textContent = 'Sage — Internal Knowledge Base'; const subtitle = document.createElement('p'); subtitle.className = 'home-subtitle'; subtitle.textContent = 'Runbooks, reference guides, and procedures for Axiom Works infrastructure.'; home.appendChild(h1); home.appendChild(subtitle); const grid = document.createElement('div'); grid.className = 'home-grid'; navIndex.categories.forEach(cat => { const catLabel = document.createElement('div'); catLabel.className = 'home-category-label'; catLabel.textContent = cat.label; home.appendChild(catLabel); const catGrid = document.createElement('div'); catGrid.className = 'home-grid'; cat.articles.forEach(id => { const article = allArticles.find(a => a.id === id); if (!article) return; const card = document.createElement('div'); card.className = 'home-card'; card.innerHTML = `
${esc(article.title)}
${esc(article.summary)}
`; card.onclick = () => showArticle(id); catGrid.appendChild(card); }); home.appendChild(catGrid); }); main.appendChild(home); main.scrollTop = 0; } // ── Article ─────────────────────────────────────────────────────────────────── function showArticle(id) { const article = allArticles.find(a => a.id === id); if (!article) return; currentArticleId = id; clearSearch(); setActiveNav(id); const main = document.getElementById('main'); main.classList.remove('hidden'); document.getElementById('search-results').classList.remove('visible'); const catLabel = CATEGORY_LABELS[article.category] ?? article.category; let html = `

${esc(article.title)}

${esc(catLabel)} ${esc(article.updated)}

${esc(article.summary)}

`; article.sections.forEach(section => { html += `
`; if (section.heading) { html += `

${esc(section.heading)}

`; } if (section.body) { html += `
${section.body}
`; } if (section.code) { html += `
${esc(section.code)}
`; } html += `
`; }); if (article.tags?.length) { html += `
${article.tags.map(t => `${esc(t)}`).join('')}
`; } main.innerHTML = html; main.scrollTop = 0; } // ── Search ──────────────────────────────────────────────────────────────────── let searchTimer = null; function onSearchInput(e) { const q = e.target.value.trim(); clearTimeout(searchTimer); if (!q) { clearSearch(); return; } searchTimer = setTimeout(() => runSearch(q), 120); } function clearSearch() { document.getElementById('search').value = ''; document.getElementById('search-results').classList.remove('visible'); const main = document.getElementById('main'); main.classList.remove('hidden'); } function runSearch(q) { const terms = q.toLowerCase().split(/\s+/).filter(Boolean); const results = []; for (const article of allArticles) { const haystack = [ article.title, article.summary, ...(article.tags ?? []), ...article.sections.flatMap(s => [s.heading ?? '', textFromHtml(s.body ?? ''), s.code ?? '']) ].join(' ').toLowerCase(); const score = terms.filter(t => haystack.includes(t)).length; if (score === 0) continue; // Find a snippet with the first matching term let snippet = article.summary; const firstTerm = terms[0]; for (const section of article.sections) { const text = textFromHtml(section.body ?? '') + ' ' + (section.code ?? ''); const idx = text.toLowerCase().indexOf(firstTerm); if (idx !== -1) { const start = Math.max(0, idx - 60); const end = Math.min(text.length, idx + 120); snippet = (start > 0 ? '…' : '') + text.slice(start, end) + (end < text.length ? '…' : ''); break; } } results.push({ article, score, snippet }); } results.sort((a, b) => b.score - a.score); const container = document.getElementById('search-results'); container.classList.add('visible'); document.getElementById('main').classList.add('hidden'); if (results.length === 0) { container.innerHTML = `
No articles matched "${esc(q)}".
`; return; } container.innerHTML = results.map(({ article, snippet }) => { const highlighted = highlightTerms(esc(snippet), terms); return `
${esc(article.title)}
${highlighted}
`; }).join(''); container.querySelectorAll('.search-result').forEach(el => { el.onclick = () => showArticle(el.dataset.id); }); } function highlightTerms(text, terms) { let out = text; terms.forEach(term => { const re = new RegExp(`(${escapeRegex(term)})`, 'gi'); out = out.replace(re, '$1'); }); return out; } // ── Utilities ───────────────────────────────────────────────────────────────── function esc(str) { return String(str ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function textFromHtml(html) { const div = document.createElement('div'); div.innerHTML = html; return div.textContent ?? ''; } function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // ── Init ────────────────────────────────────────────────────────────────────── async function init() { const main = document.getElementById('main'); main.innerHTML = '

Loading…

'; try { await loadData(); } catch (err) { main.innerHTML = `

Failed to load knowledge base: ${esc(err.message)}

`; return; } buildNav(); showHome(); document.getElementById('search').addEventListener('input', onSearchInput); // Keyboard shortcut: / to focus search document.addEventListener('keydown', e => { if (e.key === '/' && document.activeElement !== document.getElementById('search')) { e.preventDefault(); document.getElementById('search').focus(); } if (e.key === 'Escape') { clearSearch(); if (currentArticleId) setActiveNav(currentArticleId); else showHome(); } }); } document.addEventListener('DOMContentLoaded', init);