diff --git a/docker-compose.yml b/docker-compose.yml index b6c27d8..01266eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,13 +8,11 @@ services: environment: - PORT=8395 - DB_PATH=/app/data/tasks.db - - WIKI_DIR=/home/bear/.openclaw/workspace/wiki + - WIKI_DIR=/app/wiki + - AGENTS_DIR=/app/agents + - OPENCLAW_CONFIG=/app/config/openclaw.json volumes: - ./data:/app/data - - /home/bear/.openclaw/workspace/wiki:/home/bear/.openclaw/workspace/wiki - networks: - - proxy-net - -networks: - proxy-net: - external: true + - /home/bear/.openclaw/workspace/wiki:/app/wiki + - /home/bear/.openclaw/agents:/app/agents:ro + - /home/bear/.openclaw/openclaw.json:/app/config/openclaw.json:ro diff --git a/public/app.js b/public/app.js index 5c9e9eb..a9dbee0 100644 --- a/public/app.js +++ b/public/app.js @@ -1,141 +1,272 @@ -const STATUSES = ['Backlog', 'Todo', 'In Progress', 'Review', 'Done']; +// Navigation +const navLinks = document.querySelectorAll('.nav-link'); +const pages = document.querySelectorAll('.page'); -const board = document.getElementById('board'); -const template = document.getElementById('task-template'); -const form = document.getElementById('task-form'); - -let tasks = []; - -function renderBoard() { - board.innerHTML = ''; - - for (const status of STATUSES) { - const column = document.createElement('section'); - column.className = 'column'; - - const heading = document.createElement('h2'); - heading.textContent = `${status} (${tasks.filter((t) => t.status === status).length})`; - - const cards = document.createElement('div'); - cards.className = 'cards'; - - tasks - .filter((task) => task.status === status) - .forEach((task) => cards.appendChild(renderCard(task))); - - column.appendChild(heading); - column.appendChild(cards); - board.appendChild(column); - } -} - -function renderCard(task) { - const node = template.content.firstElementChild.cloneNode(true); - - node.querySelector('.card-title').textContent = task.title; - node.querySelector('.card-desc').textContent = task.description || 'No description'; - node.querySelector('.assignee').textContent = `Assignee: ${task.assignee || 'Unassigned'}`; - node.querySelector('.tags').textContent = `Tags: ${(task.tags || []).join(', ') || 'None'}`; - - const badge = node.querySelector('.priority'); - badge.textContent = task.priority; - badge.classList.add((task.priority || '').toLowerCase()); - - const statusSelect = node.querySelector('.status-select'); - statusSelect.value = task.status; - statusSelect.addEventListener('change', async () => { - await updateTask(task.id, { status: statusSelect.value }); +navLinks.forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const targetPage = link.dataset.page; + + // Update active nav link + navLinks.forEach(l => l.classList.remove('active')); + link.classList.add('active'); + + // Show target page + pages.forEach(page => { + page.classList.remove('active'); + if (page.id === `page-${targetPage}`) { + page.classList.add('active'); + } + }); + + // Load page data + if (targetPage === 'wiki') loadWiki(); + if (targetPage === 'agents') loadAgents(); + if (targetPage === 'usage') loadUsage(); }); +}); - return node; -} +// Task Dashboard +const COLUMNS = { + 'Backlog': { title: '📋 Backlog', tasks: [] }, + 'Todo': { title: '📝 Todo', tasks: [] }, + 'In Progress': { title: '🔄 In Progress', tasks: [] }, + 'Review': { title: '👀 Review', tasks: [] }, + 'Done': { title: '✅ Done', tasks: [] } +}; async function loadTasks() { const res = await fetch('/api/tasks'); - tasks = await res.json(); + const tasks = await res.json(); + + // Reset columns + Object.keys(COLUMNS).forEach(status => { + COLUMNS[status].tasks = []; + }); + + // Group tasks by status + tasks.forEach(task => { + if (COLUMNS[task.status]) { + COLUMNS[task.status].tasks.push(task); + } + }); + renderBoard(); } -async function createTask(payload) { - const res = await fetch('/api/tasks', { +function renderBoard() { + const board = document.getElementById('board'); + board.innerHTML = ''; + + Object.entries(COLUMNS).forEach(([status, column]) => { + const columnEl = document.createElement('div'); + columnEl.className = 'column'; + + columnEl.innerHTML = ` +
${escapeHtml(task.description || '')}
+ + + + `; + + // Checkbox handler + const checkbox = cardEl.querySelector('.card-check'); + checkbox.addEventListener('change', async () => { + await fetch(`/api/tasks/${task.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'Done' }) + }); + loadTasks(); + }); + + cardsEl.appendChild(cardEl); + }); + + board.appendChild(columnEl); + }); +} + +// Task form +document.getElementById('task-form').addEventListener('submit', async (e) => { + e.preventDefault(); + + const formData = new FormData(e.target); + const task = { + title: formData.get('title'), + description: formData.get('description'), + assignee: formData.get('assignee'), + priority: formData.get('priority'), + status: formData.get('status'), + tags: formData.get('tags').split(',').map(t => t.trim()).filter(t => t) + }; + + await fetch('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), + body: JSON.stringify(task) }); - - if (!res.ok) { - const data = await res.json().catch(() => ({})); - throw new Error(data.error || 'failed_to_create_task'); - } - - return res.json(); -} - -async function updateTask(id, payload) { - const res = await fetch(`/api/tasks/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - - if (!res.ok) { - const data = await res.json().catch(() => ({})); - throw new Error(data.error || 'failed_to_update_task'); - } - - return res.json(); -} - -form.addEventListener('submit', async (e) => { - e.preventDefault(); - - const payload = { - title: form.title.value, - description: form.description.value, - assignee: form.assignee.value, - priority: form.priority.value, - status: form.status.value, - tags: form.tags.value - .split(',') - .map((s) => s.trim()) - .filter(Boolean), - }; - - try { - await createTask(payload); - form.reset(); - form.priority.value = 'Medium'; - form.status.value = 'Backlog'; - } catch (err) { - alert(err.message); - } + + e.target.reset(); + loadTasks(); }); -function upsertTask(task) { - const idx = tasks.findIndex((t) => t.id === task.id); - if (idx === -1) { - tasks.unshift(task); - } else { - tasks[idx] = task; +// Wiki +async function loadWiki() { + const res = await fetch('/api/wiki'); + const pages = await res.json(); + + const wikiList = document.getElementById('wiki-list'); + wikiList.innerHTML = ''; + + pages.forEach(page => { + const itemEl = document.createElement('div'); + itemEl.className = 'wiki-item'; + itemEl.innerHTML = ` +${new Date(page.created).toLocaleDateString()}
+ `; + + itemEl.addEventListener('click', async () => { + // Mark active + wikiList.querySelectorAll('.wiki-item').forEach(i => i.classList.remove('active')); + itemEl.classList.add('active'); + + // Load content + const contentRes = await fetch(`/api/wiki/${page.filename}`); + const contentData = await contentRes.json(); + + const wikiContent = document.getElementById('wiki-content'); + wikiContent.innerHTML = `${escapeHtml(contentData.content)}`;
+ });
+
+ wikiList.appendChild(itemEl);
+ });
+}
+
+// Agents
+async function loadAgents() {
+ const res = await fetch('/api/agents');
+ const agents = await res.json();
+
+ const grid = document.getElementById('agents-grid');
+ grid.innerHTML = '';
+
+ agents.forEach(agent => {
+ const cardEl = document.createElement('div');
+ cardEl.className = 'agent-card';
+
+ cardEl.innerHTML = `
+ ${agent.currentTask || 'No active task'}
+No provider data available
'; + return; } - renderBoard(); + + usage.providers.forEach(provider => { + const cardEl = document.createElement('div'); + cardEl.className = 'usage-card'; + + cardEl.innerHTML = ` +Real-time task coordination board
-Real-time task coordination board
+Task documentation and implementation details
+Select a wiki page to view documentation
+Fleet agent workspace and configuration
+Provider models, quotas, and limits
+