// Command Center SPA Router const sections = document.querySelectorAll('.panel'); const links = document.querySelectorAll('.nav-links a'); function navigate(hash) { const target = hash.replace('#', '') || 'agents'; sections.forEach(s => s.hidden = s.id !== target); links.forEach(l => l.classList.toggle('active', l.getAttribute('href') === '#' + target)); // Init config panel when navigated to if (target === 'config') { initConfigPanel(); } } links.forEach(l => l.addEventListener('click', e => { e.preventDefault(); window.location.hash = l.getAttribute('href'); })); window.addEventListener('hashchange', () => navigate(window.location.hash)); navigate(window.location.hash || '#agents'); // ── Config File Editor ──────────────────────────────────────────────── let configState = { files: [], currentFile: null, currentContent: '', currentPath: '', dirty: false, lastSaved: null, }; function initConfigPanel() { if (configState.files.length === 0) { loadFileList(); } } async function loadFileList() { const container = document.getElementById('config-files-container'); container.innerHTML = '
Loading files...
'; try { const res = await fetch('/api/config/list'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); configState.files = data.files; renderFileList(data.files); } catch (err) { container.innerHTML = `Failed to load files: ${err.message}
`; } } function renderFileList(files) { const container = document.getElementById('config-files-container'); const countEl = document.getElementById('config-count'); countEl.textContent = `${files.length} file${files.length !== 1 ? 's' : ''}`; if (files.length === 0) { container.innerHTML = 'No config files found.
'; return; } container.innerHTML = ''; files.forEach(f => { const item = document.createElement('div'); item.className = 'config-file-item'; if (configState.currentPath === f.path) { item.classList.add('selected'); } item.dataset.path = f.path; const nameSpan = document.createElement('span'); nameSpan.className = 'file-item-name'; nameSpan.textContent = f.label || f.filename; const metaSpan = document.createElement('span'); metaSpan.className = 'file-item-meta'; if (f.missing) { metaSpan.textContent = 'missing'; metaSpan.style.color = 'var(--text-muted)'; } else { metaSpan.textContent = formatFileSize(f.size) + ' · ' + f.line_count + ' lines'; } item.appendChild(nameSpan); item.appendChild(metaSpan); item.addEventListener('click', () => openFile(f.path)); container.appendChild(item); }); } function formatFileSize(bytes) { if (bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB']; const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), 2); return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i]; } async function openFile(path) { configState.dirty = false; configState.currentPath = path; configState.lastSaved = null; // Show editor view document.getElementById('config-file-list').hidden = true; document.getElementById('config-editor').hidden = false; document.getElementById('config-editor-textarea').disabled = true; document.getElementById('config-editor-textarea').value = 'Loading...'; document.getElementById('editor-line-numbers').innerHTML = ''; document.getElementById('editor-filename').textContent = ''; document.getElementById('status-path').textContent = 'Loading...'; document.getElementById('status-meta').textContent = ''; document.getElementById('status-saved').textContent = ''; try { const res = await fetch('/api/config/read?path=' + encodeURIComponent(path)); if (!res.ok) { const err = await res.json(); throw new Error(err.error || `HTTP ${res.status}`); } const data = await res.json(); configState.currentFile = data; configState.currentContent = data.content; const filename = data.label || data.filename; document.getElementById('editor-filename').textContent = filename; document.getElementById('config-editor-textarea').value = data.content; document.getElementById('config-editor-textarea').disabled = false; document.getElementById('config-editor-textarea').focus(); // Status bar document.getElementById('status-path').textContent = data.path; document.getElementById('status-meta').textContent = formatFileSize(data.size) + ' · ' + data.line_count + ' lines'; document.getElementById('status-saved').textContent = data.last_modified ? 'Last modified: ' + formatTimestamp(data.last_modified) : ''; updateLineNumbers(); document.getElementById('config-editor-textarea').addEventListener('input', onEditorChange); document.getElementById('config-editor-textarea').addEventListener('scroll', syncScroll); document.getElementById('config-editor-textarea').addEventListener('keydown', handleEditorKeydown); // Highlight selected in file list renderFileList(configState.files); } catch (err) { showToast('Error loading file: ' + err.message, 'error'); document.getElementById('config-editor-textarea').value = ''; document.getElementById('config-editor-textarea').disabled = true; } } function onEditorChange() { configState.dirty = true; updateLineNumbers(); updateSaveButton(); } function updateSaveButton() { const btn = document.getElementById('config-save-btn'); if (configState.dirty) { btn.textContent = 'Save'; btn.style.background = 'var(--accent)'; } else { btn.textContent = 'Saved ✓'; btn.style.background = 'var(--success)'; } } function syncScroll() { const textarea = document.getElementById('config-editor-textarea'); const lineNumbers = document.getElementById('editor-line-numbers'); lineNumbers.scrollTop = textarea.scrollTop; } function handleEditorKeydown(e) { // Tab key support if (e.key === 'Tab') { e.preventDefault(); const textarea = document.getElementById('config-editor-textarea'); const start = textarea.selectionStart; const end = textarea.selectionEnd; const val = textarea.value; textarea.value = val.substring(0, start) + ' ' + val.substring(end); textarea.selectionStart = textarea.selectionEnd = start + 4; onEditorChange(); } // Ctrl+S / Cmd+S if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveFile(); } } function updateLineNumbers() { const textarea = document.getElementById('config-editor-textarea'); const lineNumbers = document.getElementById('editor-line-numbers'); const lines = textarea.value.split('\n'); const count = lines.length; let html = ''; for (let i = 1; i <= count; i++) { html += '