/* ═══════════════════════════════════════════════════════════ AETHEEL MISSION CONTROL — CLIENT APP ═══════════════════════════════════════════════════════════ */ const API = ''; // Same origin const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); // ── State ──────────────────────────────────────────────── let currentPage = 'command-center'; let sseSource = null; let uptimeInterval = null; let statsData = {}; let activityData = []; // ── Router ─────────────────────────────────────────────── function getPage() { const hash = window.location.hash.slice(1) || '/'; const routes = { '/': 'command-center', '/activity': 'activity', '/sessions': 'sessions', '/brain': 'brain', '/productivity': 'productivity', '/content': 'content', '/tasks': 'tasks', '/connections': 'connections', '/settings': 'settings', }; return routes[hash] || 'command-center'; } function navigateTo(page) { currentPage = page; $$('.nav-item').forEach(item => { item.classList.toggle('active', item.dataset.page === page); }); renderPage(); } window.addEventListener('hashchange', () => navigateTo(getPage())); // ── Init ───────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { navigateTo(getPage()); startSSE(); startUptimePolling(); }); // ── SSE (Real-time events) ─────────────────────────────── function startSSE() { if (sseSource) sseSource.close(); sseSource = new EventSource(`${API}/events`); sseSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'connected') { updateStatus(true); return; } activityData.unshift(data); if (activityData.length > 200) activityData = activityData.slice(0, 200); if (currentPage === 'command-center' || currentPage === 'activity') { const feedEl = $('#activity-feed'); if (feedEl) { feedEl.insertAdjacentHTML('afterbegin', renderActivityItem(data)); while (feedEl.children.length > 100) feedEl.removeChild(feedEl.lastChild); } } if (currentPage === 'command-center') fetchStats(); } catch { /* ignore */ } }; sseSource.onerror = () => { updateStatus(false); setTimeout(() => { if (sseSource?.readyState === EventSource.CLOSED) startSSE(); }, 5000); }; } function updateStatus(online) { const dot = $('.status-dot'); const text = $('.status-text'); if (dot) dot.classList.toggle('offline', !online); if (text) text.textContent = online ? 'Agent Online' : 'Disconnected'; } // ── Uptime Polling ─────────────────────────────────────── function startUptimePolling() { fetchStats(); if (uptimeInterval) clearInterval(uptimeInterval); uptimeInterval = setInterval(fetchStats, 10000); } async function fetchStats() { try { const res = await fetch(`${API}/api/stats`); statsData = await res.json(); const uptimeEl = $('#xp-uptime'); const fillEl = $('#xp-fill'); if (uptimeEl) uptimeEl.textContent = statsData.uptimeFormatted; const hours = statsData.uptimeMs / 3600000; const pct = Math.min(100, (hours / 24) * 100); if (fillEl) fillEl.style.width = pct + '%'; updateStatCards(); } catch { /* ignore */ } } function updateStatCards() { const els = { 'stat-messages': statsData.messagesHandled, 'stat-heartbeats': statsData.heartbeats, 'stat-cron': statsData.cronRuns, 'stat-uptime': statsData.uptimeFormatted, }; for (const [id, val] of Object.entries(els)) { const el = document.getElementById(id); if (el) el.textContent = val ?? 0; } } // ── API Helpers ────────────────────────────────────────── async function apiFetch(path) { const res = await fetch(`${API}${path}`); return res.json(); } async function apiPost(path, data) { const res = await fetch(`${API}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); return res.json(); } async function apiDelete(path) { const res = await fetch(`${API}${path}`, { method: 'DELETE' }); return res.json(); } // ── Page Renderer ──────────────────────────────────────── async function renderPage() { const container = $('#page-container'); container.innerHTML = '
'; switch (currentPage) { case 'command-center': return renderCommandCenter(container); case 'activity': return renderActivity(container); case 'sessions': return renderSessions(container); case 'brain': return renderBrain(container); case 'productivity': return renderProductivity(container); case 'content': return renderContentIntel(container); case 'tasks': return renderTasks(container); case 'connections': return renderConnections(container); case 'settings': return renderSettings(container); } } // ═══════════════════════════════════════════════════════════ // COMMAND CENTER // ═══════════════════════════════════════════════════════════ async function renderCommandCenter(el) { const [stats, activity, config] = await Promise.all([ apiFetch('/api/stats'), apiFetch('/api/activity?count=30'), apiFetch('/api/config'), ]); statsData = stats; activityData = activity; el.innerHTML = `
Messages Handled
${stats.messagesHandled}
+${stats.messagesHandled} total
Heartbeats
${stats.heartbeats}
pulsing
Cron Runs
${stats.cronRuns}
scheduled
Agent Uptime
${stats.uptimeFormatted}
online
Live Activity Feed ${activity.length} recent
${activity.map(renderActivityItem).join('')} ${activity.length === 0 ? '
📡
Waiting for events…
' : ''}
Agent Configuration
${renderConfigRow('Backend', config.agentBackend)} ${renderConfigRow('CLI Path', config.backendCliPath)} ${renderConfigRow('Model', config.backendModel)} ${renderConfigRow('Max Turns', config.backendMaxTurns)} ${renderConfigRow('Timeout', (config.queryTimeoutMs / 1000) + 's')} ${renderConfigRow('Max Concurrent', config.maxConcurrentQueries)} ${renderConfigRow('Queue Depth', config.maxQueueDepth)} ${renderConfigRow('Permission Mode', config.permissionMode)} ${renderConfigRow('Output Channel', config.outputChannelId)}
`; } // ═══════════════════════════════════════════════════════════ // ACTIVITY // ═══════════════════════════════════════════════════════════ async function renderActivity(el) { const activity = await apiFetch('/api/activity?count=100'); activityData = activity; el.innerHTML = `
Event Log ${activity.length} events
${activity.map(renderActivityItem).join('')} ${activity.length === 0 ? '
📡
No events yet — activity will appear here as the agent processes events
' : ''}
`; } // ═══════════════════════════════════════════════════════════ // SESSIONS // ═══════════════════════════════════════════════════════════ async function renderSessions(el) { const [sessions, channels] = await Promise.all([ apiFetch('/api/sessions'), apiFetch('/api/messages'), ]); el.innerHTML = `
Active Sessions
${sessions.length}
Channels with History
${channels.length}
Session Bindings
${sessions.length > 0 ? `
${sessions.map(s => `
#${s.channelId}
${s.sessionId.slice(0, 16)}…
`).join('')}
` : '
💬
No active sessions
'}
${channels.length > 0 ? `
Message History Channels
${channels.map(c => `
#${c}
Click to view →
`).join('')}
` : ''}
`; } async function viewChannelMessages(channelId) { const messages = await apiFetch(`/api/messages?channelId=${channelId}`); const viewer = $('#message-viewer'); if (!viewer) return; viewer.innerHTML = `
Messages in #${channelId} ${messages.length}
${messages.map(m => `
${m.direction === 'inbound' ? '↓' : '↑'}
${escapeHtml(m.sender)}
${escapeHtml(m.content.slice(0, 200))}
${formatTime(m.timestamp)}
`).join('')}
`; } window.viewChannelMessages = viewChannelMessages; // ═══════════════════════════════════════════════════════════ // BRAIN (Second Brain) // ═══════════════════════════════════════════════════════════ async function renderBrain(el) { const [facts, memory, persona, skills] = await Promise.all([ apiFetch('/api/brain'), apiFetch('/api/memory'), apiFetch('/api/persona'), apiFetch('/api/skills'), ]); const categories = [...new Set(facts.map(f => f.category))]; el.innerHTML = `
Stored Facts
${facts.length}
indexed
Categories
${categories.length}
organized
Skills
${skills.length}
loaded
Memory Lines
${(memory.content || '').split('\n').filter(l => l.trim()).length}
stored
Add Knowledge
Search & Browse ${facts.length} facts
${renderBrainFacts(facts)}
`; } let selectedBrainType = 'note'; function selectBrainType(btn) { $$('.type-tab').forEach(t => t.classList.remove('active')); btn.classList.add('active'); selectedBrainType = btn.dataset.type; const input = $('#brain-content-input'); const placeholders = { note: 'Type a fact, paste a URL, or describe a file reference…', url: 'Paste a URL (e.g. https://example.com/article)', file: 'File path or reference (e.g. docs/api-spec.md)', }; input.placeholder = placeholders[selectedBrainType] || placeholders.note; } window.selectBrainType = selectBrainType; async function addBrainFact() { const content = $('#brain-content-input')?.value?.trim(); if (!content) return; const category = $('#brain-category-input')?.value?.trim() || 'general'; const tagsStr = $('#brain-tags-input')?.value?.trim() || ''; const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : []; await apiPost('/api/brain', { content, type: selectedBrainType, category, tags, }); // Reset inputs $('#brain-content-input').value = ''; $('#brain-category-input').value = ''; $('#brain-tags-input').value = ''; // Refresh facts list const facts = await apiFetch('/api/brain'); const listEl = $('#brain-facts-list'); if (listEl) listEl.innerHTML = renderBrainFacts(facts); } window.addBrainFact = addBrainFact; async function deleteBrainFact(id) { await apiDelete(`/api/brain?id=${id}`); const facts = await apiFetch('/api/brain'); const listEl = $('#brain-facts-list'); if (listEl) listEl.innerHTML = renderBrainFacts(facts); } window.deleteBrainFact = deleteBrainFact; async function searchBrainFacts() { const query = $('#brain-search')?.value?.trim() || ''; const url = query ? `/api/brain?q=${encodeURIComponent(query)}` : '/api/brain'; const facts = await apiFetch(url); const listEl = $('#brain-facts-list'); if (listEl) listEl.innerHTML = renderBrainFacts(facts); } window.searchBrainFacts = searchBrainFacts; function renderBrainFacts(facts) { if (facts.length === 0) return '
🧠
No facts stored yet — add your first knowledge item above
'; return facts.map(f => `
${f.type === 'url' ? '🔗' : f.type === 'file' ? '📁' : '📝'} ${f.type} ${escapeHtml(f.category)}
${f.type === 'url' ? `${escapeHtml(f.content)}` : escapeHtml(f.content)}
${f.tags.length > 0 ? `
${f.tags.map(t => `${escapeHtml(t)}`).join('')}
` : ''}
${formatTime(f.createdAt)}
`).join(''); } function switchBrainTab(btn, tab) { $$('.brain-tab').forEach(t => t.classList.remove('active')); btn.classList.add('active'); ['facts', 'memory', 'persona', 'skills'].forEach(t => { const el = document.getElementById(`brain-${t}`); if (el) el.style.display = t === tab ? 'block' : 'none'; }); } window.switchBrainTab = switchBrainTab; async function saveMemory() { const btn = $('#save-memory-btn'); const content = $('#memory-editor').value; btn.textContent = 'Saving…'; await apiPost('/api/memory', { content }); btn.textContent = '✓ Saved'; btn.classList.add('saved'); setTimeout(() => { btn.textContent = 'Save Memory'; btn.classList.remove('saved'); }, 2000); } window.saveMemory = saveMemory; async function savePersona() { const btn = $('#save-persona-btn'); const content = $('#persona-editor').value; btn.textContent = 'Saving…'; await apiPost('/api/persona', { content }); btn.textContent = '✓ Saved'; btn.classList.add('saved'); setTimeout(() => { btn.textContent = 'Save Persona'; btn.classList.remove('saved'); }, 2000); } window.savePersona = savePersona; // ═══════════════════════════════════════════════════════════ // PRODUCTIVITY // ═══════════════════════════════════════════════════════════ async function renderProductivity(el) { const tasks = await apiFetch('/api/tasks'); const byStatus = { todo: [], 'in-progress': [], done: [], archived: [] }; tasks.forEach(t => { if (byStatus[t.status]) byStatus[t.status].push(t); }); const projects = [...new Set(tasks.map(t => t.project))]; el.innerHTML = `
To Do
${byStatus.todo.length}
queued
In Progress
${byStatus['in-progress'].length}
active
Done
${byStatus.done.length}
completed
Projects
${projects.length}
tracked
Add Task
${renderTaskColumn('📥 To Do', byStatus.todo, 'todo')} ${renderTaskColumn('🔄 In Progress', byStatus['in-progress'], 'in-progress')} ${renderTaskColumn('✅ Done', byStatus.done, 'done')}
`; } function renderTaskColumn(title, tasks, status) { return `
${title} ${tasks.length}
${tasks.length > 0 ? tasks.map(t => renderProductivityCard(t)).join('') : `
No tasks
`}
`; } function renderProductivityCard(task) { const priorityColors = { low: 'green', medium: 'blue', high: 'orange', urgent: 'red' }; const nextStatus = { todo: 'in-progress', 'in-progress': 'done', done: 'archived' }; const nextLabel = { todo: 'Start →', 'in-progress': 'Done ✓', done: 'Archive' }; return `
${task.priority} ${task.project ? `${escapeHtml(task.project)}` : ''}
${escapeHtml(task.title)}
${task.description ? `
${escapeHtml(task.description)}
` : ''} ${task.dueDate ? `
📅 ${task.dueDate}
` : ''}
`; } async function addProductivityTask() { const title = $('#task-title')?.value?.trim(); if (!title) return; await apiPost('/api/tasks', { title, description: $('#task-description')?.value?.trim() || '', priority: $('#task-priority')?.value || 'medium', project: $('#task-project')?.value?.trim() || 'default', dueDate: $('#task-due')?.value || null, }); renderPage(); } window.addProductivityTask = addProductivityTask; async function moveTask(id, newStatus) { await apiPost('/api/tasks/update', { id, status: newStatus }); renderPage(); } window.moveTask = moveTask; async function deleteProductivityTask(id) { await apiDelete(`/api/tasks?id=${id}`); renderPage(); } window.deleteProductivityTask = deleteProductivityTask; // ═══════════════════════════════════════════════════════════ // CONTENT INTEL // ═══════════════════════════════════════════════════════════ async function renderContentIntel(el) { const items = await apiFetch('/api/content'); const byStatus = { queued: [], read: [], archived: [] }; items.forEach(i => { if (byStatus[i.status]) byStatus[i.status].push(i); }); const typeIcons = { article: '📄', video: '🎬', tweet: '🐦', paper: '📑', repo: '💻', other: '📎', }; el.innerHTML = `
Queued
${byStatus.queued.length}
to read
Read
${byStatus.read.length}
processed
Total
${items.length}
items
Add Content
Content Library ${items.length} items
${renderContentItems(items, typeIcons)}
`; } function renderContentItems(items, typeIcons) { if (items.length === 0) return '
📰
No content saved yet — add articles, papers, and more above
'; return items.map(item => `
${typeIcons[item.type] || '📎'}
${item.url ? `${escapeHtml(item.title || item.url)}` : escapeHtml(item.title)}
${escapeHtml(item.source)} · ${formatTime(item.savedAt)}
${item.status === 'queued' ? `` : ''} ${item.status === 'read' ? `` : ''}
${item.summary ? `
${escapeHtml(item.summary)}
` : ''} ${item.tags.length > 0 ? `
${item.tags.map(t => `${escapeHtml(t)}`).join('')}
` : ''}
`).join(''); } async function addContentItem() { const title = $('#content-title')?.value?.trim(); const url = $('#content-url')?.value?.trim(); if (!title && !url) return; await apiPost('/api/content', { title: title || url, url: url || '', type: $('#content-type')?.value || 'article', source: $('#content-source')?.value?.trim() || 'manual', summary: $('#content-summary')?.value?.trim() || '', tags: ($('#content-tags')?.value || '').split(',').map(t => t.trim()).filter(Boolean), }); // Reset ['content-title', 'content-url', 'content-source', 'content-tags', 'content-summary'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); const items = await apiFetch('/api/content'); const listEl = $('#content-list'); if (listEl) listEl.innerHTML = renderContentItems(items, { article: '📄', video: '🎬', tweet: '🐦', paper: '📑', repo: '💻', other: '📎' }); } window.addContentItem = addContentItem; async function updateContent(id, status) { await apiPost('/api/content/update', { id, status }); renderPage(); } window.updateContent = updateContent; async function deleteContent(id) { await apiDelete(`/api/content?id=${id}`); renderPage(); } window.deleteContent = deleteContent; async function searchContent() { const query = $('#content-search')?.value?.trim() || ''; const url = query ? `/api/content?q=${encodeURIComponent(query)}` : '/api/content'; const items = await apiFetch(url); const listEl = $('#content-list'); if (listEl) listEl.innerHTML = renderContentItems(items, { article: '📄', video: '🎬', tweet: '🐦', paper: '📑', repo: '💻', other: '📎' }); } window.searchContent = searchContent; // ═══════════════════════════════════════════════════════════ // TASKS (Scheduler) // ═══════════════════════════════════════════════════════════ async function renderTasks(el) { const [cron, heartbeats, hooks] = await Promise.all([ apiFetch('/api/cron'), apiFetch('/api/heartbeats'), apiFetch('/api/hooks'), ]); const allTasks = [ ...cron.map(j => ({ ...j, taskType: 'cron', schedule: j.expression })), ...heartbeats.map(h => ({ ...h, taskType: 'heartbeat', schedule: `every ${h.intervalSeconds}s` })), ]; el.innerHTML = `
Cron Jobs
${cron.length}
Heartbeat Checks
${heartbeats.length}
Hooks Defined
${Object.keys(hooks).filter(k => hooks[k]).length}
Scheduled Tasks ${allTasks.length} active
${allTasks.length > 0 ? `
${allTasks.map(t => `
${t.taskType} ${escapeHtml(t.name)} ${escapeHtml(t.instruction)} ${escapeHtml(t.schedule)}
`).join('')}
` : '
No scheduled tasks — define them in agents.md and heartbeat.md
'}
${Object.keys(hooks).filter(k => hooks[k]).length > 0 ? `
Lifecycle Hooks
${Object.entries(hooks).filter(([, v]) => v).map(([hookType, instruction]) => `
hook ${escapeHtml(hookType)} ${escapeHtml(instruction)}
`).join('')}
` : ''} `; } // ═══════════════════════════════════════════════════════════ // CONNECTIONS // ═══════════════════════════════════════════════════════════ async function renderConnections(el) { const connections = await apiFetch('/api/connections'); const active = connections.filter(c => c.status === 'active').length; const iconMap = { discord: '🎮', claude: '🤖', codex: '💻', gemini: '✨', opencode: '🔧', heartbeat: '💚', cron: '🕐', ipc: '📨', skills: '⚡', }; el.innerHTML = `
${active} / ${connections.length} connected ${Math.round((active / connections.length) * 100)}%
${connections.map(c => `
${escapeHtml(c.name)}
${escapeHtml(c.detail)}
${c.status}
`).join('')}
`; } // ═══════════════════════════════════════════════════════════ // SETTINGS // ═══════════════════════════════════════════════════════════ async function renderSettings(el) { const config = await apiFetch('/api/config'); const todos = JSON.parse(localStorage.getItem('mc-todos') || '[]'); el.innerHTML = `
Configuration
${Object.entries(config).map(([k, v]) => renderConfigRow(formatConfigKey(k), v)).join('')}
Quick Todos ${todos.filter(t => !t.done).length} pending
${todos.map((t, i) => renderTodoItem(t, i)).join('')}
`; } function addTodo() { const input = $('#todo-input'); const text = input.value.trim(); if (!text) return; const todos = JSON.parse(localStorage.getItem('mc-todos') || '[]'); todos.push({ text, done: false }); localStorage.setItem('mc-todos', JSON.stringify(todos)); input.value = ''; renderPage(); } window.addTodo = addTodo; function toggleTodo(index) { const todos = JSON.parse(localStorage.getItem('mc-todos') || '[]'); if (todos[index]) { todos[index].done = !todos[index].done; localStorage.setItem('mc-todos', JSON.stringify(todos)); renderPage(); } } window.toggleTodo = toggleTodo; function deleteTodo(index) { const todos = JSON.parse(localStorage.getItem('mc-todos') || '[]'); todos.splice(index, 1); localStorage.setItem('mc-todos', JSON.stringify(todos)); renderPage(); } window.deleteTodo = deleteTodo; function renderTodoItem(todo, index) { return `
${escapeHtml(todo.text)}
`; } // ═══════════════════════════════════════════════════════════ // SHARED RENDER HELPERS // ═══════════════════════════════════════════════════════════ function renderActivityItem(item) { const icons = { message: '💬', heartbeat: '💚', cron: '🕐', hook: '🪝', webhook: '🌐' }; return `
${icons[item.type] || '⚡'}
${capitalize(item.type)} — ${escapeHtml(item.source)}
${escapeHtml(item.detail)}
${formatTime(item.timestamp)}
`; } function renderConfigRow(key, value) { return `
${escapeHtml(key)} ${escapeHtml(String(value ?? '—'))}
`; } function formatConfigKey(key) { return key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()).trim(); } // ═══════════════════════════════════════════════════════════ // UTILITIES // ═══════════════════════════════════════════════════════════ function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function formatTime(isoStr) { try { const d = new Date(isoStr); const now = new Date(); const diffMs = now - d; if (diffMs < 60000) return `${Math.floor(diffMs / 1000)}s ago`; if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)}m ago`; if (diffMs < 86400000) return `${Math.floor(diffMs / 3600000)}h ago`; return d.toLocaleDateString(); } catch { return ''; } }