// 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 += '
' + i + '
'; } lineNumbers.innerHTML = html; } function goBackToList() { if (configState.dirty) { if (!confirm('You have unsaved changes. Discard them?')) { return; } } document.getElementById('config-file-list').hidden = false; document.getElementById('config-editor').hidden = true; configState.currentFile = null; configState.currentContent = ''; configState.currentPath = ''; configState.dirty = false; renderFileList(configState.files); } async function saveFile() { if (!configState.dirty) return; const textarea = document.getElementById('config-editor-textarea'); const content = textarea.value; const path = configState.currentPath; // Confirmation dialog if (!confirm('Save changes to ' + (configState.currentFile?.label || path) + '?')) { return; } const btn = document.getElementById('config-save-btn'); btn.disabled = true; btn.textContent = 'Saving...'; try { const res = await fetch('/api/config/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path, content }), }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || `HTTP ${res.status}`); } configState.dirty = false; configState.lastSaved = new Date(); configState.currentContent = content; // Update status document.getElementById('status-meta').textContent = formatFileSize(data.size) + ' · ' + data.line_count + ' lines'; document.getElementById('status-saved').textContent = 'Saved: ' + formatTimestamp(new Date().toISOString()); showToast('File saved successfully. Backup: ' + data.backup_path, 'success'); updateSaveButton(); // Refresh file list metadata loadFileList(); } catch (err) { showToast('Save failed: ' + err.message, 'error'); } finally { btn.disabled = false; } } function formatTimestamp(iso) { try { const d = new Date(iso); return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', }); } catch { return iso; } } function showToast(message, type) { const toast = document.getElementById('config-toast'); toast.textContent = message; toast.className = 'config-toast toast-' + (type || 'info'); toast.hidden = false; // Position toast const editor = document.querySelector('.config-editor'); if (!editor.hidden) { const rect = editor.getBoundingClientRect(); toast.style.top = (rect.top + 20) + 'px'; toast.style.left = '50%'; toast.style.transform = 'translateX(-50%)'; } clearTimeout(toast._timeout); toast._timeout = setTimeout(() => { toast.hidden = true; }, 4000); } // ── Event listeners ────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { document.getElementById('config-save-btn').addEventListener('click', saveFile); document.getElementById('config-back-btn').addEventListener('click', goBackToList); });