Files
aetheel-2/dashboard/app.js

1140 lines
46 KiB
JavaScript

/* ═══════════════════════════════════════════════════════════
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 = '<div class="empty-state"><div class="empty-icon">⏳</div></div>';
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 = `
<div class="page-header">
<h1 class="page-title">Command Center</h1>
<p class="page-subtitle">Real-time overview of your agent</p>
</div>
<div class="stats-grid">
<div class="stat-card orange">
<div class="stat-label">Messages Handled</div>
<div class="stat-value" id="stat-messages">${stats.messagesHandled}</div>
<span class="stat-badge orange">+${stats.messagesHandled} total</span>
</div>
<div class="stat-card green">
<div class="stat-label">Heartbeats</div>
<div class="stat-value" id="stat-heartbeats">${stats.heartbeats}</div>
<span class="stat-badge green">pulsing</span>
</div>
<div class="stat-card purple">
<div class="stat-label">Cron Runs</div>
<div class="stat-value" id="stat-cron">${stats.cronRuns}</div>
<span class="stat-badge blue">scheduled</span>
</div>
<div class="stat-card blue">
<div class="stat-label">Agent Uptime</div>
<div class="stat-value" id="stat-uptime">${stats.uptimeFormatted}</div>
<span class="stat-badge green">online</span>
</div>
</div>
<div class="section-grid">
<div class="section-card">
<div class="section-header">
<span class="section-title">Live Activity Feed</span>
<span class="section-badge">${activity.length} recent</span>
</div>
<div class="section-body">
<div class="activity-list" id="activity-feed">
${activity.map(renderActivityItem).join('')}
${activity.length === 0 ? '<div class="empty-state"><div class="empty-icon">📡</div><div class="empty-text">Waiting for events…</div></div>' : ''}
</div>
</div>
</div>
<div class="section-card">
<div class="section-header">
<span class="section-title">Agent Configuration</span>
</div>
<div class="section-body">
<div class="config-table">
${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)}
</div>
</div>
</div>
</div>
`;
}
// ═══════════════════════════════════════════════════════════
// ACTIVITY
// ═══════════════════════════════════════════════════════════
async function renderActivity(el) {
const activity = await apiFetch('/api/activity?count=100');
activityData = activity;
el.innerHTML = `
<div class="page-header">
<h1 class="page-title">Activity</h1>
<p class="page-subtitle">Full event history — updates in real time</p>
</div>
<div class="section-card" style="animation-delay:0.1s; opacity:0;">
<div class="section-header">
<span class="section-title">Event Log</span>
<span class="section-badge">${activity.length} events</span>
</div>
<div class="section-body" style="max-height: 70vh;">
<div class="activity-list" id="activity-feed">
${activity.map(renderActivityItem).join('')}
${activity.length === 0 ? '<div class="empty-state"><div class="empty-icon">📡</div><div class="empty-text">No events yet — activity will appear here as the agent processes events</div></div>' : ''}
</div>
</div>
</div>
`;
}
// ═══════════════════════════════════════════════════════════
// SESSIONS
// ═══════════════════════════════════════════════════════════
async function renderSessions(el) {
const [sessions, channels] = await Promise.all([
apiFetch('/api/sessions'),
apiFetch('/api/messages'),
]);
el.innerHTML = `
<div class="page-header">
<h1 class="page-title">Sessions</h1>
<p class="page-subtitle">Active Discord channel sessions</p>
</div>
<div class="stats-grid">
<div class="stat-card blue">
<div class="stat-label">Active Sessions</div>
<div class="stat-value">${sessions.length}</div>
</div>
<div class="stat-card green">
<div class="stat-label">Channels with History</div>
<div class="stat-value">${channels.length}</div>
</div>
</div>
<div class="section-card fade-in">
<div class="section-header">
<span class="section-title">Session Bindings</span>
</div>
<div class="section-body">
${sessions.length > 0 ? `
<div class="session-list">
${sessions.map(s => `
<div class="session-item">
<div><div class="session-channel">#${s.channelId}</div></div>
<div class="session-id">${s.sessionId.slice(0, 16)}…</div>
</div>
`).join('')}
</div>
` : '<div class="empty-state"><div class="empty-icon">💬</div><div class="empty-text">No active sessions</div></div>'}
</div>
</div>
${channels.length > 0 ? `
<div class="section-card fade-in" style="margin-top: 20px;">
<div class="section-header">
<span class="section-title">Message History Channels</span>
</div>
<div class="section-body">
<div class="session-list">
${channels.map(c => `
<div class="session-item" style="cursor:pointer" onclick="viewChannelMessages('${c}')">
<div class="session-channel">#${c}</div>
<span style="font-size:12px; color: var(--text-muted);">Click to view →</span>
</div>
`).join('')}
</div>
</div>
</div>
` : ''}
<div id="message-viewer" style="margin-top: 20px;"></div>
`;
}
async function viewChannelMessages(channelId) {
const messages = await apiFetch(`/api/messages?channelId=${channelId}`);
const viewer = $('#message-viewer');
if (!viewer) return;
viewer.innerHTML = `
<div class="section-card fade-in">
<div class="section-header">
<span class="section-title">Messages in #${channelId}</span>
<span class="section-badge">${messages.length}</span>
</div>
<div class="section-body" style="max-height: 50vh;">
<div class="activity-list">
${messages.map(m => `
<div class="activity-item">
<div class="activity-icon ${m.direction === 'inbound' ? 'message' : 'hook'}">
${m.direction === 'inbound' ? '↓' : '↑'}
</div>
<div class="activity-body">
<div class="activity-title">${escapeHtml(m.sender)}</div>
<div class="activity-detail">${escapeHtml(m.content.slice(0, 200))}</div>
</div>
<div class="activity-time">${formatTime(m.timestamp)}</div>
</div>
`).join('')}
</div>
</div>
</div>
`;
}
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 = `
<div class="page-header">
<h1 class="page-title">🧠 Second Brain</h1>
<p class="page-subtitle">Your agent's knowledge base — stored facts, memory files, and skills</p>
</div>
<div class="stats-grid">
<div class="stat-card orange">
<div class="stat-label">Stored Facts</div>
<div class="stat-value">${facts.length}</div>
<span class="stat-badge orange">indexed</span>
</div>
<div class="stat-card blue">
<div class="stat-label">Categories</div>
<div class="stat-value">${categories.length}</div>
<span class="stat-badge blue">organized</span>
</div>
<div class="stat-card green">
<div class="stat-label">Skills</div>
<div class="stat-value">${skills.length}</div>
<span class="stat-badge green">loaded</span>
</div>
<div class="stat-card purple">
<div class="stat-label">Memory Lines</div>
<div class="stat-value">${(memory.content || '').split('\n').filter(l => l.trim()).length}</div>
<span class="stat-badge purple">stored</span>
</div>
</div>
<div class="brain-tabs">
<button class="brain-tab active" onclick="switchBrainTab(this, 'facts')">📌 Facts</button>
<button class="brain-tab" onclick="switchBrainTab(this, 'memory')">📝 Memory</button>
<button class="brain-tab" onclick="switchBrainTab(this, 'persona')">🎭 Persona</button>
<button class="brain-tab" onclick="switchBrainTab(this, 'skills')">⚡ Skills</button>
</div>
<!-- FACTS TAB -->
<div id="brain-facts">
<div class="section-card fade-in">
<div class="section-header">
<span class="section-title">Add Knowledge</span>
</div>
<div class="section-body">
<div class="brain-type-tabs">
<button class="type-tab active" data-type="note" onclick="selectBrainType(this)">📝 Quick Note</button>
<button class="type-tab" data-type="url" onclick="selectBrainType(this)">🔗 URL</button>
<button class="type-tab" data-type="file" onclick="selectBrainType(this)">📁 File Ref</button>
</div>
<div class="brain-add-form">
<textarea id="brain-content-input" class="brain-input" placeholder="Type a fact, paste a URL, or describe a file reference…" rows="3"></textarea>
<div class="brain-form-row">
<input id="brain-category-input" class="brain-input-small" type="text" placeholder="Category (e.g. research, tools)" />
<input id="brain-tags-input" class="brain-input-small" type="text" placeholder="Tags (comma separated)" />
<button class="btn primary" onclick="addBrainFact()">+ Add</button>
</div>
</div>
</div>
</div>
<div class="section-card fade-in" style="margin-top: 16px;">
<div class="section-header">
<span class="section-title">Search & Browse</span>
<span class="section-badge">${facts.length} facts</span>
</div>
<div class="section-body">
<div class="brain-search-row">
<input id="brain-search" class="brain-input-full" type="text" placeholder="Search facts…" oninput="searchBrainFacts()" />
</div>
<div id="brain-facts-list" class="brain-facts-list">
${renderBrainFacts(facts)}
</div>
</div>
</div>
</div>
<!-- MEMORY TAB -->
<div id="brain-memory" style="display:none;">
<textarea class="editor-area" id="memory-editor">${escapeHtml(memory.content || '')}</textarea>
<div class="editor-actions">
<button class="btn primary" id="save-memory-btn" onclick="saveMemory()">Save Memory</button>
</div>
</div>
<!-- PERSONA TAB -->
<div id="brain-persona" style="display:none;">
<textarea class="editor-area" id="persona-editor">${escapeHtml(persona.content || '')}</textarea>
<div class="editor-actions">
<button class="btn primary" id="save-persona-btn" onclick="savePersona()">Save Persona</button>
</div>
</div>
<!-- SKILLS TAB -->
<div id="brain-skills" style="display:none;">
${skills.length > 0 ? `
<div class="skills-grid">
${skills.map(s => `
<div class="skill-card">
<div class="skill-name">${escapeHtml(s.name)}</div>
<div class="skill-preview">${escapeHtml(s.content.slice(0, 200))}</div>
</div>
`).join('')}
</div>
` : '<div class="empty-state"><div class="empty-icon">⚡</div><div class="empty-text">No skills loaded — add SKILL.md files to config/skills/</div></div>'}
</div>
`;
}
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 '<div class="empty-state"><div class="empty-icon">🧠</div><div class="empty-text">No facts stored yet — add your first knowledge item above</div></div>';
return facts.map(f => `
<div class="brain-fact-card">
<div class="brain-fact-header">
<span class="brain-fact-type ${f.type}">${f.type === 'url' ? '🔗' : f.type === 'file' ? '📁' : '📝'} ${f.type}</span>
<span class="brain-fact-category">${escapeHtml(f.category)}</span>
<button class="brain-fact-delete" onclick="deleteBrainFact('${f.id}')" title="Delete">✕</button>
</div>
<div class="brain-fact-content">${f.type === 'url' ? `<a href="${escapeHtml(f.content)}" target="_blank" rel="noopener">${escapeHtml(f.content)}</a>` : escapeHtml(f.content)}</div>
${f.tags.length > 0 ? `<div class="brain-fact-tags">${f.tags.map(t => `<span class="brain-tag">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
<div class="brain-fact-time">${formatTime(f.createdAt)}</div>
</div>
`).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 = `
<div class="page-header">
<h1 class="page-title">📋 Productivity</h1>
<p class="page-subtitle">Task management — track your agent's workload</p>
</div>
<div class="stats-grid">
<div class="stat-card blue">
<div class="stat-label">To Do</div>
<div class="stat-value">${byStatus.todo.length}</div>
<span class="stat-badge blue">queued</span>
</div>
<div class="stat-card orange">
<div class="stat-label">In Progress</div>
<div class="stat-value">${byStatus['in-progress'].length}</div>
<span class="stat-badge orange">active</span>
</div>
<div class="stat-card green">
<div class="stat-label">Done</div>
<div class="stat-value">${byStatus.done.length}</div>
<span class="stat-badge green">completed</span>
</div>
<div class="stat-card purple">
<div class="stat-label">Projects</div>
<div class="stat-value">${projects.length}</div>
<span class="stat-badge purple">tracked</span>
</div>
</div>
<div class="section-card fade-in">
<div class="section-header">
<span class="section-title">Add Task</span>
</div>
<div class="section-body">
<div class="productivity-add-form">
<input id="task-title" class="brain-input-full" type="text" placeholder="Task title…" />
<div class="brain-form-row">
<input id="task-description" class="brain-input-small" type="text" placeholder="Description (optional)" />
<select id="task-priority" class="brain-input-small">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
<input id="task-project" class="brain-input-small" type="text" placeholder="Project" />
<input id="task-due" class="brain-input-small" type="date" />
<button class="btn primary" onclick="addProductivityTask()">+ Add</button>
</div>
</div>
</div>
</div>
<div class="productivity-columns">
${renderTaskColumn('📥 To Do', byStatus.todo, 'todo')}
${renderTaskColumn('🔄 In Progress', byStatus['in-progress'], 'in-progress')}
${renderTaskColumn('✅ Done', byStatus.done, 'done')}
</div>
`;
}
function renderTaskColumn(title, tasks, status) {
return `
<div class="task-column">
<div class="task-column-header">
<span>${title}</span>
<span class="section-badge">${tasks.length}</span>
</div>
<div class="task-column-body">
${tasks.length > 0 ? tasks.map(t => renderProductivityCard(t)).join('') : `<div class="empty-state-small">No tasks</div>`}
</div>
</div>
`;
}
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 `
<div class="productivity-card">
<div class="productivity-card-header">
<span class="priority-badge ${priorityColors[task.priority] || 'blue'}">${task.priority}</span>
${task.project ? `<span class="project-badge">${escapeHtml(task.project)}</span>` : ''}
</div>
<div class="productivity-card-title">${escapeHtml(task.title)}</div>
${task.description ? `<div class="productivity-card-desc">${escapeHtml(task.description)}</div>` : ''}
${task.dueDate ? `<div class="productivity-card-due">📅 ${task.dueDate}</div>` : ''}
<div class="productivity-card-actions">
<button class="btn-small" onclick="moveTask('${task.id}', '${nextStatus[task.status]}')">${nextLabel[task.status]}</button>
<button class="btn-small danger" onclick="deleteProductivityTask('${task.id}')">✕</button>
</div>
</div>
`;
}
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 = `
<div class="page-header">
<h1 class="page-title">📰 Content Intel</h1>
<p class="page-subtitle">Curate and track content for your agent</p>
</div>
<div class="stats-grid">
<div class="stat-card blue">
<div class="stat-label">Queued</div>
<div class="stat-value">${byStatus.queued.length}</div>
<span class="stat-badge blue">to read</span>
</div>
<div class="stat-card green">
<div class="stat-label">Read</div>
<div class="stat-value">${byStatus.read.length}</div>
<span class="stat-badge green">processed</span>
</div>
<div class="stat-card purple">
<div class="stat-label">Total</div>
<div class="stat-value">${items.length}</div>
<span class="stat-badge purple">items</span>
</div>
</div>
<div class="section-card fade-in">
<div class="section-header">
<span class="section-title">Add Content</span>
</div>
<div class="section-body">
<div class="brain-add-form">
<div class="brain-form-row">
<input id="content-title" class="brain-input-small" type="text" placeholder="Title" />
<input id="content-url" class="brain-input-small" type="url" placeholder="https://…" />
</div>
<div class="brain-form-row">
<select id="content-type" class="brain-input-small">
<option value="article">📄 Article</option>
<option value="video">🎬 Video</option>
<option value="tweet">🐦 Tweet</option>
<option value="paper">📑 Paper</option>
<option value="repo">💻 Repo</option>
<option value="other">📎 Other</option>
</select>
<input id="content-source" class="brain-input-small" type="text" placeholder="Source (e.g. HN, Twitter)" />
<input id="content-tags" class="brain-input-small" type="text" placeholder="Tags (comma sep)" />
<button class="btn primary" onclick="addContentItem()">+ Add</button>
</div>
<textarea id="content-summary" class="brain-input" placeholder="Summary or notes (optional)" rows="2"></textarea>
</div>
</div>
</div>
<div class="section-card fade-in" style="margin-top: 16px;">
<div class="section-header">
<span class="section-title">Content Library</span>
<span class="section-badge">${items.length} items</span>
</div>
<div class="section-body">
<div class="brain-search-row">
<input id="content-search" class="brain-input-full" type="text" placeholder="Search content…" oninput="searchContent()" />
</div>
<div id="content-list" class="content-list">
${renderContentItems(items, typeIcons)}
</div>
</div>
</div>
`;
}
function renderContentItems(items, typeIcons) {
if (items.length === 0) return '<div class="empty-state"><div class="empty-icon">📰</div><div class="empty-text">No content saved yet — add articles, papers, and more above</div></div>';
return items.map(item => `
<div class="content-card ${item.status}">
<div class="content-card-header">
<span class="content-type-icon">${typeIcons[item.type] || '📎'}</span>
<div class="content-card-info">
<div class="content-card-title">${item.url ? `<a href="${escapeHtml(item.url)}" target="_blank" rel="noopener">${escapeHtml(item.title || item.url)}</a>` : escapeHtml(item.title)}</div>
<div class="content-card-meta">${escapeHtml(item.source)} · ${formatTime(item.savedAt)}</div>
</div>
<div class="content-card-actions">
${item.status === 'queued' ? `<button class="btn-small" onclick="updateContent('${item.id}', 'read')">Mark Read</button>` : ''}
${item.status === 'read' ? `<button class="btn-small" onclick="updateContent('${item.id}', 'archived')">Archive</button>` : ''}
<button class="btn-small danger" onclick="deleteContent('${item.id}')">✕</button>
</div>
</div>
${item.summary ? `<div class="content-card-summary">${escapeHtml(item.summary)}</div>` : ''}
${item.tags.length > 0 ? `<div class="brain-fact-tags">${item.tags.map(t => `<span class="brain-tag">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
</div>
`).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 = `
<div class="page-header">
<h1 class="page-title">⏱️ Scheduler</h1>
<p class="page-subtitle">Scheduled cron jobs, heartbeat checks, and lifecycle hooks</p>
</div>
<div class="stats-grid">
<div class="stat-card purple">
<div class="stat-label">Cron Jobs</div>
<div class="stat-value">${cron.length}</div>
</div>
<div class="stat-card green">
<div class="stat-label">Heartbeat Checks</div>
<div class="stat-value">${heartbeats.length}</div>
</div>
<div class="stat-card orange">
<div class="stat-label">Hooks Defined</div>
<div class="stat-value">${Object.keys(hooks).filter(k => hooks[k]).length}</div>
</div>
</div>
<div class="section-card fade-in">
<div class="section-header">
<span class="section-title">Scheduled Tasks</span>
<span class="section-badge">${allTasks.length} active</span>
</div>
<div class="section-body">
${allTasks.length > 0 ? `
<div class="task-list">
${allTasks.map(t => `
<div class="task-item">
<span class="task-type-badge ${t.taskType}">${t.taskType}</span>
<span class="task-name">${escapeHtml(t.name)}</span>
<span class="task-detail">${escapeHtml(t.instruction)}</span>
<span class="task-schedule">${escapeHtml(t.schedule)}</span>
</div>
`).join('')}
</div>
` : '<div class="empty-state"><div class="empty-icon">⏰</div><div class="empty-text">No scheduled tasks — define them in agents.md and heartbeat.md</div></div>'}
</div>
</div>
${Object.keys(hooks).filter(k => hooks[k]).length > 0 ? `
<div class="section-card fade-in" style="margin-top: 20px;">
<div class="section-header">
<span class="section-title">Lifecycle Hooks</span>
</div>
<div class="section-body">
<div class="task-list">
${Object.entries(hooks).filter(([, v]) => v).map(([hookType, instruction]) => `
<div class="task-item">
<span class="task-type-badge cron">hook</span>
<span class="task-name">${escapeHtml(hookType)}</span>
<span class="task-detail">${escapeHtml(instruction)}</span>
</div>
`).join('')}
</div>
</div>
</div>
` : ''}
`;
}
// ═══════════════════════════════════════════════════════════
// 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 = `
<div class="page-header">
<h1 class="page-title">Connections</h1>
<p class="page-subtitle">All integrations and services powering your agent</p>
</div>
<div class="progress-bar-container fade-in">
<div class="progress-label">
<span>${active} / ${connections.length} connected</span>
<span>${Math.round((active / connections.length) * 100)}%</span>
</div>
<div class="progress-track">
<div class="progress-fill" style="width: ${(active / connections.length) * 100}%;"></div>
</div>
</div>
<div class="connections-grid">
${connections.map(c => `
<div class="connection-card ${c.status === 'inactive' ? 'inactive' : ''}">
<div class="connection-logo ${c.logo}">
${iconMap[c.logo] || '🔌'}
</div>
<div class="connection-info">
<div class="connection-name">${escapeHtml(c.name)}</div>
<div class="connection-detail">${escapeHtml(c.detail)}</div>
</div>
<span class="connection-status ${c.status}">${c.status}</span>
</div>
`).join('')}
</div>
`;
}
// ═══════════════════════════════════════════════════════════
// SETTINGS
// ═══════════════════════════════════════════════════════════
async function renderSettings(el) {
const config = await apiFetch('/api/config');
const todos = JSON.parse(localStorage.getItem('mc-todos') || '[]');
el.innerHTML = `
<div class="page-header">
<h1 class="page-title">Settings</h1>
<p class="page-subtitle">Agent configuration and quick todos</p>
</div>
<div class="section-grid">
<div class="section-card fade-in">
<div class="section-header">
<span class="section-title">Configuration</span>
</div>
<div class="section-body">
<div class="config-table">
${Object.entries(config).map(([k, v]) => renderConfigRow(formatConfigKey(k), v)).join('')}
</div>
</div>
</div>
<div class="section-card fade-in" style="animation-delay: 0.1s; opacity: 0;">
<div class="section-header">
<span class="section-title">Quick Todos</span>
<span class="section-badge">${todos.filter(t => !t.done).length} pending</span>
</div>
<div class="section-body">
<div class="todo-input-row">
<input class="todo-input" id="todo-input" type="text" placeholder="Add a todo…" onkeydown="if(event.key==='Enter')addTodo()" />
<button class="btn primary" onclick="addTodo()">Add</button>
</div>
<div class="todo-list" id="todo-list">
${todos.map((t, i) => renderTodoItem(t, i)).join('')}
</div>
</div>
</div>
</div>
`;
}
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 `
<div class="todo-item">
<div class="todo-checkbox ${todo.done ? 'checked' : ''}" onclick="toggleTodo(${index})"></div>
<span class="todo-text ${todo.done ? 'completed' : ''}">${escapeHtml(todo.text)}</span>
<button class="todo-delete" onclick="deleteTodo(${index})">✕</button>
</div>
`;
}
// ═══════════════════════════════════════════════════════════
// SHARED RENDER HELPERS
// ═══════════════════════════════════════════════════════════
function renderActivityItem(item) {
const icons = { message: '💬', heartbeat: '💚', cron: '🕐', hook: '🪝', webhook: '🌐' };
return `
<div class="activity-item">
<div class="activity-icon ${item.type}">
${icons[item.type] || '⚡'}
</div>
<div class="activity-body">
<div class="activity-title">${capitalize(item.type)}${escapeHtml(item.source)}</div>
<div class="activity-detail">${escapeHtml(item.detail)}</div>
</div>
<div class="activity-time">${formatTime(item.timestamp)}</div>
</div>
`;
}
function renderConfigRow(key, value) {
return `
<div class="config-row">
<span class="config-key">${escapeHtml(key)}</span>
<span class="config-value">${escapeHtml(String(value ?? '—'))}</span>
</div>
`;
}
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 '';
}
}