scheduled tasks

This commit is contained in:
pavel 2026-02-10 19:32:33 +01:00
commit 937827d08b
17 changed files with 1445 additions and 136 deletions

View file

@ -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();