Files
openchamber/packages/web/server/lib/scheduled-tasks/routes.js
T

182 lines
6.6 KiB
JavaScript

const asNonEmptyString = (value) => {
if (typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
const parseProjectID = (req) => asNonEmptyString(req?.params?.projectId);
const parseTaskID = (req) => asNonEmptyString(req?.params?.taskId);
export const registerScheduledTaskRoutes = (app, dependencies) => {
const {
readSettingsFromDiskMigrated,
sanitizeProjects,
projectConfigRuntime,
scheduledTasksRuntime,
getOpenChamberEventClients,
writeSseEvent,
scheduledTaskService = createScheduledTaskService(dependencies),
} = dependencies;
app.get('/api/projects/:projectId/scheduled-tasks', async (req, res) => {
const projectID = parseProjectID(req);
if (!projectID) {
return res.status(400).json({ error: 'projectId is required' });
}
try {
const tasks = await scheduledTaskService.list(projectID);
return res.json({ tasks });
} catch (error) {
if (error?.statusCode) return res.status(error.statusCode).json({ error: error.message });
console.error('[ScheduledTasks] failed to load tasks:', error);
return res.status(500).json({ error: 'Failed to load scheduled tasks' });
}
});
app.put('/api/projects/:projectId/scheduled-tasks', async (req, res) => {
const projectID = parseProjectID(req);
if (!projectID) {
return res.status(400).json({ error: 'projectId is required' });
}
const taskInput = req.body && typeof req.body === 'object' ? req.body.task : null;
if (!taskInput || typeof taskInput !== 'object') {
return res.status(400).json({ error: 'task payload is required' });
}
try {
return res.json(await scheduledTaskService.upsert(projectID, taskInput));
} catch (error) {
if (error?.statusCode) return res.status(error.statusCode).json({ error: error.message });
const message = error instanceof Error ? error.message : 'Failed to save scheduled task';
const statusCode = message.toLowerCase().includes('required') || message.toLowerCase().includes('invalid')
? 400
: 500;
if (statusCode === 500) {
console.error('[ScheduledTasks] failed to save task:', error);
}
return res.status(statusCode).json({ error: message });
}
});
app.delete('/api/projects/:projectId/scheduled-tasks/:taskId', async (req, res) => {
const projectID = parseProjectID(req);
const taskID = parseTaskID(req);
if (!projectID) {
return res.status(400).json({ error: 'projectId is required' });
}
if (!taskID) {
return res.status(400).json({ error: 'taskId is required' });
}
try {
return res.json({ tasks: await scheduledTaskService.remove(projectID, taskID) });
} catch (error) {
if (error?.statusCode) return res.status(error.statusCode).json({ error: error.message });
console.error('[ScheduledTasks] failed to delete task:', error);
return res.status(500).json({ error: 'Failed to delete scheduled task' });
}
});
app.patch('/api/projects/:projectId/scheduled-tasks/:taskId/loop-file', async (req, res) => {
const projectID = parseProjectID(req);
const taskID = parseTaskID(req);
if (!projectID) return res.status(400).json({ error: 'projectId is required' });
if (!taskID) return res.status(400).json({ error: 'taskId is required' });
try {
const task = await scheduledTaskService.setLoopEnabled(projectID, taskID, req.body?.enabled);
return res.json({ task });
} catch (error) {
if (error?.statusCode) return res.status(error.statusCode).json({ error: error.message });
console.error('[ScheduledTasks] failed to update loop file:', error);
return res.status(500).json({ error: 'Failed to update loop file' });
}
});
app.delete('/api/projects/:projectId/scheduled-tasks/:taskId/loop-file', async (req, res) => {
const projectID = parseProjectID(req);
const taskID = parseTaskID(req);
if (!projectID) return res.status(400).json({ error: 'projectId is required' });
if (!taskID) return res.status(400).json({ error: 'taskId is required' });
try {
return res.json({ tasks: await scheduledTaskService.removeLoopFile(projectID, taskID) });
} catch (error) {
if (error?.statusCode) return res.status(error.statusCode).json({ error: error.message });
console.error('[ScheduledTasks] failed to delete loop file:', error);
return res.status(500).json({ error: 'Failed to delete loop file' });
}
});
app.post('/api/projects/:projectId/scheduled-tasks/:taskId/run', async (req, res) => {
const projectID = parseProjectID(req);
const taskID = parseTaskID(req);
if (!projectID) {
return res.status(400).json({ error: 'projectId is required' });
}
if (!taskID) {
return res.status(400).json({ error: 'taskId is required' });
}
try {
return res.json({ ok: true, ...await scheduledTaskService.run(projectID, taskID) });
} catch (error) {
if (error?.statusCode) return res.status(error.statusCode).json({ error: error.message, ...(error.task ? { task: error.task } : {}) });
console.error('[ScheduledTasks] failed to run task:', error);
return res.status(500).json({ error: 'Failed to run scheduled task' });
}
});
app.get('/api/openchamber/scheduled-tasks/status', async (_req, res) => {
try {
return res.json(await scheduledTaskService.status());
} catch (error) {
console.error('[ScheduledTasks] failed to resolve scheduled task status:', error);
return res.status(500).json({ error: 'Failed to resolve scheduled task status' });
}
});
app.get('/api/openchamber/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders?.();
const clients = getOpenChamberEventClients();
clients.add(res);
try {
writeSseEvent(res, {
type: 'openchamber:event-stream-ready',
properties: {
connectedAt: Date.now(),
},
});
} catch {
}
const heartbeat = setInterval(() => {
try {
writeSseEvent(res, {
type: 'openchamber:heartbeat',
properties: {
timestamp: Date.now(),
},
});
} catch {
clearInterval(heartbeat);
clients.delete(res);
}
}, 25_000);
req.on('close', () => {
clearInterval(heartbeat);
clients.delete(res);
});
});
};
import { createScheduledTaskService } from './service.js';