scheduled tasks
This commit is contained in:
parent
0fa627ca6d
commit
937827d08b
17 changed files with 1445 additions and 136 deletions
|
|
@ -1,8 +1,14 @@
|
|||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
const API_URL = 'http://localhost:3000';
|
||||
|
||||
const state = {
|
||||
tasks: [],
|
||||
selectedTaskId: null,
|
||||
selectedRunId: null,
|
||||
currentView: 'dashboard', // 'dashboard' or 'task'
|
||||
isEditing: false,
|
||||
};
|
||||
|
||||
// DOM elements
|
||||
|
|
@ -19,17 +25,59 @@ const viewStatusEl = document.getElementById('view-status');
|
|||
const viewDateEl = document.getElementById('view-date');
|
||||
const answerContainerEl = document.getElementById('answer-container');
|
||||
const answerOutputEl = document.getElementById('answer-output');
|
||||
const rerunBtn = document.getElementById('rerun-btn');
|
||||
const runListEl = document.getElementById('run-list');
|
||||
const toggleLogsBtn = document.getElementById('toggle-logs-btn');
|
||||
const logsContainerEl = document.getElementById('logs-container');
|
||||
const dashboardViewEl = document.getElementById('dashboard-view');
|
||||
const recentRunsListEl = document.getElementById('recent-runs-list');
|
||||
const logoLink = document.getElementById('logo-link');
|
||||
const editTaskBtn = document.getElementById('edit-task-btn');
|
||||
const modalTitle = document.getElementById('modal-title');
|
||||
const submitTaskBtn = document.getElementById('submit-task-btn');
|
||||
const goalTextarea = document.getElementById('goal-textarea');
|
||||
const cronInput = document.getElementById('cron-input');
|
||||
const schedulePresets = document.getElementById('schedule-presets');
|
||||
const toggleCustomCronBtn = document.getElementById('toggle-custom-cron');
|
||||
const customCronContainer = document.getElementById('custom-cron-container');
|
||||
const presetBtns = document.querySelectorAll('.btn-preset');
|
||||
|
||||
async function fetchTasks() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks`);
|
||||
state.tasks = await response.json();
|
||||
const newTasks = await response.json();
|
||||
|
||||
// Check if we should follow the latest run (if we were already watching it)
|
||||
let shouldFollowLatest = false;
|
||||
if (state.selectedTaskId) {
|
||||
const currentTask = state.tasks.find(t => t.id === state.selectedTaskId);
|
||||
if (currentTask && currentTask.runs && currentTask.runs.length > 0) {
|
||||
const latestRunId = currentTask.runs[currentTask.runs.length - 1].id;
|
||||
if (state.selectedRunId === latestRunId) {
|
||||
shouldFollowLatest = true;
|
||||
}
|
||||
} else if (!state.selectedRunId) {
|
||||
shouldFollowLatest = true;
|
||||
}
|
||||
}
|
||||
|
||||
state.tasks = newTasks;
|
||||
renderTaskList();
|
||||
|
||||
// Update current view if a task is selected
|
||||
// If we are on the dashboard, refresh it too
|
||||
if (state.currentView === 'dashboard') {
|
||||
fetchRecentRuns();
|
||||
}
|
||||
|
||||
// If a task is selected, update it
|
||||
if (state.selectedTaskId) {
|
||||
const task = state.tasks.find(t => t.id === state.selectedTaskId);
|
||||
const task = state.tasks.find((t) => t.id === state.selectedTaskId);
|
||||
if (task) {
|
||||
if (shouldFollowLatest && task.runs && task.runs.length > 0) {
|
||||
state.selectedRunId = task.runs[task.runs.length - 1].id;
|
||||
}
|
||||
|
||||
renderRunHistory(task);
|
||||
showTaskView(task);
|
||||
}
|
||||
}
|
||||
|
|
@ -38,19 +86,45 @@ async function fetchTasks() {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchRecentRuns() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/runs/recent`);
|
||||
const recentRuns = await response.json();
|
||||
renderDashboard(recentRuns);
|
||||
} catch (error) {
|
||||
console.error('Error fetching recent runs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTaskList() {
|
||||
taskListEl.innerHTML = state.tasks
|
||||
.map(
|
||||
(task) => `
|
||||
<li class="task-item ${state.selectedTaskId === task.id ? 'active' : ''}" data-id="${task.id}">
|
||||
<div class="task-item-title">${task.goal}</div>
|
||||
<div class="task-item-meta">
|
||||
<span class="status-dot ${task.status}"></span>
|
||||
<span>${new Date(task.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
const sortedTasks = [...state.tasks].sort((a, b) => {
|
||||
const aDate = a.runs && a.runs.length > 0
|
||||
? new Date(a.runs[a.runs.length - 1].created_at)
|
||||
: new Date(a.created_at);
|
||||
const bDate = b.runs && b.runs.length > 0
|
||||
? new Date(b.runs[b.runs.length - 1].created_at)
|
||||
: new Date(b.created_at);
|
||||
return bDate - aDate;
|
||||
});
|
||||
|
||||
taskListEl.innerHTML = sortedTasks
|
||||
.map((task) => {
|
||||
const latestRun = task.runs && task.runs.length > 0
|
||||
? task.runs[task.runs.length - 1]
|
||||
: null;
|
||||
const status = latestRun ? latestRun.status : 'pending';
|
||||
const date = latestRun ? new Date(latestRun.created_at) : new Date(task.created_at);
|
||||
|
||||
return `
|
||||
<li class="task-item ${state.selectedTaskId === task.id ? 'active' : ''}" data-id="${task.id}">
|
||||
<div class="task-item-title">${task.goal}</div>
|
||||
<div class="task-item-meta">
|
||||
<span class="status-dot ${status}"></span>
|
||||
<span>${date.toLocaleDateString()} ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
// Add event listeners
|
||||
|
|
@ -61,31 +135,120 @@ function renderTaskList() {
|
|||
});
|
||||
}
|
||||
|
||||
function selectTask(id) {
|
||||
function selectTask(id, runId = null) {
|
||||
state.selectedTaskId = id;
|
||||
state.currentView = 'task';
|
||||
const task = state.tasks.find((t) => t.id === id);
|
||||
if (!task) return;
|
||||
|
||||
if (runId) {
|
||||
state.selectedRunId = runId;
|
||||
} else if (task.runs && task.runs.length > 0) {
|
||||
// Default to latest run if not specified
|
||||
state.selectedRunId = task.runs[task.runs.length - 1].id;
|
||||
} else {
|
||||
state.selectedRunId = null;
|
||||
}
|
||||
|
||||
renderTaskList();
|
||||
renderRunHistory(task);
|
||||
showTaskView(task);
|
||||
}
|
||||
|
||||
function showTaskView(task) {
|
||||
function renderRunHistory(task) {
|
||||
runListEl.innerHTML = task.runs
|
||||
.slice()
|
||||
.reverse()
|
||||
.map(
|
||||
(run, index) => `
|
||||
<li class="run-item ${state.selectedRunId === run.id ? 'active' : ''}" data-id="${run.id}">
|
||||
<span>Run #${task.runs.length - index}</span>
|
||||
<span class="run-item-date">${new Date(run.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
document.querySelectorAll('.run-item').forEach((item) => {
|
||||
item.addEventListener('click', () => {
|
||||
selectTask(task.id, item.dataset.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showDashboard() {
|
||||
state.currentView = 'dashboard';
|
||||
state.selectedTaskId = null;
|
||||
state.selectedRunId = null;
|
||||
|
||||
emptyStateEl.classList.add('hidden');
|
||||
taskViewEl.classList.add('hidden');
|
||||
dashboardViewEl.classList.remove('hidden');
|
||||
|
||||
renderTaskList();
|
||||
fetchRecentRuns();
|
||||
}
|
||||
|
||||
function renderDashboard(recentRuns) {
|
||||
recentRunsListEl.innerHTML = recentRuns
|
||||
.map(
|
||||
(run) => `
|
||||
<tr data-task-id="${run.task_id}" data-run-id="${run.id}">
|
||||
<td>${escapeHtml(run.goal)}</td>
|
||||
<td><span class="status-badge ${run.status}">${run.status}</span></td>
|
||||
<td>${new Date(run.created_at).toLocaleString()}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
document.querySelectorAll('#recent-runs-list tr').forEach((row) => {
|
||||
row.addEventListener('click', () => {
|
||||
selectTask(row.dataset.taskId, row.dataset.runId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showTaskView(task) {
|
||||
state.currentView = 'task';
|
||||
emptyStateEl.classList.add('hidden');
|
||||
dashboardViewEl.classList.add('hidden');
|
||||
taskViewEl.classList.remove('hidden');
|
||||
|
||||
const run = task.runs.find(r => r.id === state.selectedRunId) || task.runs[task.runs.length - 1];
|
||||
|
||||
viewGoalEl.textContent = task.goal;
|
||||
viewStatusEl.textContent = task.status;
|
||||
viewStatusEl.className = `status-badge ${task.status}`;
|
||||
viewDateEl.textContent = new Date(task.created_at).toLocaleDateString(undefined, {
|
||||
|
||||
// Show schedule info if exists
|
||||
const scheduleInfo = task.cron ? `<div class="schedule-badge">🕒 Scheduled: ${task.cron}</div>` : '';
|
||||
const headerMain = document.querySelector('.header-main');
|
||||
const existingBadge = headerMain.querySelector('.schedule-badge');
|
||||
if (existingBadge) existingBadge.remove();
|
||||
if (scheduleInfo) {
|
||||
headerMain.insertAdjacentHTML('beforeend', scheduleInfo);
|
||||
}
|
||||
|
||||
if (!run) {
|
||||
viewStatusEl.textContent = 'No runs';
|
||||
logsOutputEl.innerHTML = '';
|
||||
answerContainerEl.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
viewStatusEl.textContent = run.status;
|
||||
viewStatusEl.className = `status-badge ${run.status}`;
|
||||
viewDateEl.textContent = new Date(run.created_at).toLocaleString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
if (task.answer) {
|
||||
if (run.answer) {
|
||||
answerContainerEl.classList.remove('hidden');
|
||||
answerOutputEl.textContent = task.answer;
|
||||
const rawHtml = marked.parse(run.answer);
|
||||
answerOutputEl.innerHTML = DOMPurify.sanitize(rawHtml);
|
||||
} else {
|
||||
answerContainerEl.classList.add('hidden');
|
||||
}
|
||||
|
|
@ -95,7 +258,7 @@ function showTaskView(task) {
|
|||
const isFirstLoad = logsOutputEl.innerHTML === '';
|
||||
|
||||
// Simple log format
|
||||
logsOutputEl.innerHTML = task.logs
|
||||
logsOutputEl.innerHTML = run.logs
|
||||
.split('\n')
|
||||
.map((line) => `<div class="log-entry">${escapeHtml(line)}</div>`)
|
||||
.join('');
|
||||
|
|
@ -112,31 +275,140 @@ function escapeHtml(text) {
|
|||
}
|
||||
|
||||
// Event Listeners
|
||||
rerunBtn.addEventListener('click', async () => {
|
||||
if (!state.selectedTaskId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks/${state.selectedTaskId}/runs`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const updatedTask = await response.json();
|
||||
const index = state.tasks.findIndex(t => t.id === updatedTask.id);
|
||||
if (index !== -1) {
|
||||
state.tasks[index] = updatedTask;
|
||||
}
|
||||
selectTask(updatedTask.id);
|
||||
} catch (error) {
|
||||
console.error('Error re-running task:', error);
|
||||
alert('Failed to re-run task.');
|
||||
}
|
||||
});
|
||||
|
||||
newTaskBtn.addEventListener('click', () => {
|
||||
state.isEditing = false;
|
||||
modalTitle.textContent = 'New Agent Task';
|
||||
submitTaskBtn.textContent = 'Execute Directive';
|
||||
goalTextarea.value = '';
|
||||
cronInput.value = '';
|
||||
updateScheduleUI('');
|
||||
modalContainer.classList.remove('hidden');
|
||||
});
|
||||
|
||||
toggleLogsBtn.addEventListener('click', () => {
|
||||
logsContainerEl.classList.toggle('hidden');
|
||||
const isHidden = logsContainerEl.classList.contains('hidden');
|
||||
toggleLogsBtn.classList.toggle('active', !isHidden);
|
||||
toggleLogsBtn.innerHTML = isHidden ? '<span>⌨</span> Inspect Logs' : '<span>✕</span> Hide Logs';
|
||||
});
|
||||
|
||||
editTaskBtn.addEventListener('click', () => {
|
||||
const task = state.tasks.find((t) => t.id === state.selectedTaskId);
|
||||
if (!task) return;
|
||||
|
||||
state.isEditing = true;
|
||||
modalTitle.textContent = 'Edit Directive';
|
||||
submitTaskBtn.textContent = 'Update Directive';
|
||||
goalTextarea.value = task.goal;
|
||||
cronInput.value = task.cron || '';
|
||||
updateScheduleUI(task.cron || '');
|
||||
modalContainer.classList.remove('hidden');
|
||||
});
|
||||
|
||||
cancelTaskBtn.addEventListener('click', () => {
|
||||
modalContainer.classList.add('hidden');
|
||||
state.isEditing = false;
|
||||
});
|
||||
|
||||
logoLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
showDashboard();
|
||||
});
|
||||
|
||||
function updateScheduleUI(cron) {
|
||||
let matched = false;
|
||||
presetBtns.forEach(btn => {
|
||||
if (btn.dataset.cron === (cron || '')) {
|
||||
btn.classList.add('active');
|
||||
matched = true;
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
if (matched) {
|
||||
customCronContainer.classList.add('hidden');
|
||||
} else if (cron) {
|
||||
customCronContainer.classList.remove('hidden');
|
||||
} else {
|
||||
customCronContainer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
presetBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const cron = btn.dataset.cron;
|
||||
cronInput.value = cron;
|
||||
updateScheduleUI(cron);
|
||||
});
|
||||
});
|
||||
|
||||
toggleCustomCronBtn.addEventListener('click', () => {
|
||||
customCronContainer.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
cronInput.addEventListener('input', () => {
|
||||
// If user types manually, update presets active state
|
||||
updateScheduleUI(cronInput.value);
|
||||
});
|
||||
|
||||
newTaskForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(newTaskForm);
|
||||
const goal = formData.get('goal');
|
||||
const cron = formData.get('cron') || null;
|
||||
|
||||
modalContainer.classList.add('hidden');
|
||||
newTaskForm.reset();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ goal }),
|
||||
});
|
||||
const newTask = await response.json();
|
||||
state.tasks.unshift(newTask);
|
||||
selectTask(newTask.id);
|
||||
let response;
|
||||
if (state.isEditing) {
|
||||
response = await fetch(`${API_URL}/tasks/${state.selectedTaskId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ goal, cron }),
|
||||
});
|
||||
} else {
|
||||
response = await fetch(`${API_URL}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ goal, cron }),
|
||||
});
|
||||
}
|
||||
const updatedTask = await response.json();
|
||||
|
||||
if (state.isEditing) {
|
||||
const index = state.tasks.findIndex(t => t.id === updatedTask.id);
|
||||
if (index !== -1) {
|
||||
state.tasks[index] = updatedTask;
|
||||
}
|
||||
} else {
|
||||
state.tasks.unshift(updatedTask);
|
||||
}
|
||||
|
||||
state.isEditing = false;
|
||||
selectTask(updatedTask.id);
|
||||
renderTaskList();
|
||||
} catch (error) {
|
||||
console.error('Error creating task:', error);
|
||||
alert('Failed to execute task. Check console.');
|
||||
|
|
@ -144,6 +416,21 @@ newTaskForm.addEventListener('submit', async (e) => {
|
|||
});
|
||||
|
||||
// Initial load
|
||||
// Initial fetch
|
||||
fetchTasks();
|
||||
// Poll for updates every 5 seconds (simplistic for now)
|
||||
setInterval(fetchTasks, 5000);
|
||||
|
||||
// Auto-refresh every 3 seconds
|
||||
let isPolling = false;
|
||||
async function startAutoRefresh() {
|
||||
setInterval(async () => {
|
||||
if (isPolling) return;
|
||||
isPolling = true;
|
||||
try {
|
||||
await fetchTasks();
|
||||
} finally {
|
||||
isPolling = false;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
startAutoRefresh();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue