// ============================================================
// admin-docs.js — admin-only docs viewer.
//
// Lazy-loads /api/admin/docs/tree once on first tab activation,
// renders a collapsible file tree in the sidebar. Clicking a .md
// node fetches /api/admin/docs/file?path=X and renders the
// pre-sanitised HTML returned by the server (server uses `marked`).
//
// The route is gated to role=admin server-side. The sidebar tab
// button (#docs-tab-btn) is hidden by default and revealed by
// auth.js after login when user.role === 'admin'.
// ============================================================
var _inited = false;
var _treeLoaded = false;
var _tree = [];
var _flatFiles = []; // flat list of {name, path, parent} for filter
var _expanded = {}; // map of dir-path → bool, persisted in UIState
var _currentPath = '';
function $(id) { return document.getElementById(id); }
function escHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); }
// Pretty label from a file/dir basename. README is shown as "Index" so the
// entry-point doc is more obvious in the tree.
//
// Both separators, and the case is normalised rather than only capitalised:
// uppercasing the first letter of each word leaves a SHOUTING_FILENAME
// shouting, which is why CLINICAL_ASSISTANT and MODULE_CONVENTIONS sat in the
// list looking like constants next to "Learning Hub".
var ACRONYMS = { ai: 'AI', api: 'API', ui: 'UI', id: 'ID', oidc: 'OIDC', sso: 'SSO',
stt: 'STT', tts: 'TTS', pdf: 'PDF', faq: 'FAQ', mcp: 'MCP', ped: 'Ped',
openid: 'OpenID', litellm: 'LiteLLM', milvus: 'Milvus' };
var MINOR = { and: 1, or: 1, the: 1, a: 1, an: 1, of: 1, to: 1, in: 1, for: 1, with: 1 };
function prettyName(name, isDir) {
if (!isDir && /^readme\.md$/i.test(name)) return 'Index';
var base = name.replace(/\.md$/i, '');
var words = base.replace(/[-_]+/g, ' ').trim().split(/\s+/);
return words.map(function (word, i) {
var lower = word.toLowerCase();
if (ACRONYMS[lower]) return ACRONYMS[lower];
// Small joining words stay lowercase unless they open the title.
if (i > 0 && MINOR[lower]) return lower;
return lower.charAt(0).toUpperCase() + lower.slice(1);
}).join(' ');
}
// Recursively render the tree into HTML.
function renderNode(node, depth) {
if (node.type === 'dir') {
var open = _expanded[node.path] !== false; // default open
var arrow = open ? 'fa-chevron-down' : 'fa-chevron-right';
var label = prettyName(node.name, true);
var html =
'
';
return;
}
_tree = data.tree || [];
_flatFiles = [];
flatten(_tree, '');
// Restore expanded state from UIState before render
try {
var saved = window.UIState && window.UIState.get('docs.expanded');
if (saved) _expanded = JSON.parse(saved);
} catch (e) {}
renderTree();
_treeLoaded = true;
// Auto-load the top README if available, else the first file
var first = _flatFiles[0];
if (first) {
var savedPath = (window.UIState && window.UIState.get('docs.lastPath')) || first.path;
loadFile(savedPath);
markActive(savedPath);
}
})
.catch(function (err) {
$('docs-tree').innerHTML = '
Tree load failed: ' + escHtml(err.message || String(err)) + '
';
});
}
function loadFile(relPath, hash) {
var body = $('docs-reader-body');
var meta = $('docs-reader-meta');
if (!body) return;
_currentPath = relPath;
body.innerHTML = '