Changeset 0.27.2 (#169)
This commit is contained in:
251
src/js/task-admin.js
Normal file
251
src/js/task-admin.js
Normal file
@@ -0,0 +1,251 @@
|
||||
// ==========================================
|
||||
// Chat Switchboard — Task Admin Panel
|
||||
// ==========================================
|
||||
// Admin section for managing scheduled tasks, run history, and global
|
||||
// task configuration. Registered as ADMIN_LOADERS.tasks in ui-admin.js.
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var SCAFFOLD_HTML =
|
||||
'<div id="taskAdminTabs" style="display:flex;gap:8px;margin-bottom:16px">' +
|
||||
'<button class="btn-small btn-primary" id="taskTabList">Tasks</button>' +
|
||||
'<button class="btn-small" id="taskTabConfig">Configuration</button>' +
|
||||
'</div>' +
|
||||
'<div id="taskListPane">' +
|
||||
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">' +
|
||||
'<h4 style="margin:0">Scheduled Tasks</h4>' +
|
||||
'<span id="taskCount" class="badge">0</span>' +
|
||||
'</div>' +
|
||||
'<div id="taskAdminList" class="admin-list"></div>' +
|
||||
'</div>' +
|
||||
'<div id="taskConfigPane" style="display:none">' +
|
||||
'<h4 style="margin:0 0 12px">Task Configuration</h4>' +
|
||||
'<div class="settings-section">' +
|
||||
'<div class="form-row" style="gap:12px">' +
|
||||
'<label class="toggle-label"><input type="checkbox" id="taskCfgEnabled"><span class="toggle-track"></span><span>Tasks Enabled</span></label>' +
|
||||
'<label class="toggle-label"><input type="checkbox" id="taskCfgPersonal"><span class="toggle-track"></span><span>Allow Personal Tasks</span></label>' +
|
||||
'</div>' +
|
||||
'<div class="form-row" style="gap:12px;margin-top:12px">' +
|
||||
'<div class="form-group" style="flex:1"><label>Max Concurrent</label>' +
|
||||
'<input type="number" id="taskCfgMaxConcurrent" min="1" max="50" style="width:100%"></div>' +
|
||||
'<div class="form-group" style="flex:1"><label>Default Max Tokens</label>' +
|
||||
'<input type="number" id="taskCfgMaxTokens" min="1" style="width:100%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="form-row" style="gap:12px;margin-top:8px">' +
|
||||
'<div class="form-group" style="flex:1"><label>Default Max Tool Calls</label>' +
|
||||
'<input type="number" id="taskCfgMaxToolCalls" min="1" style="width:100%"></div>' +
|
||||
'<div class="form-group" style="flex:1"><label>Default Max Wall Clock (seconds)</label>' +
|
||||
'<input type="number" id="taskCfgMaxWallClock" min="10" style="width:100%"></div>' +
|
||||
'</div>' +
|
||||
'<button class="btn-small btn-primary" id="taskCfgSaveBtn" style="margin-top:12px">Save Configuration</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="taskRunPane" style="display:none">' +
|
||||
'<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">' +
|
||||
'<button class="btn-small" id="taskRunBackBtn">← Back</button>' +
|
||||
'<h4 id="taskRunTitle" style="margin:0;flex:1"></h4>' +
|
||||
'<button class="btn-small btn-danger" id="taskKillBtn">Kill Active Run</button>' +
|
||||
'</div>' +
|
||||
'<div id="taskRunList" class="admin-list"></div>' +
|
||||
'</div>';
|
||||
|
||||
function showTab(tab) {
|
||||
var el = function(id) { return document.getElementById(id); };
|
||||
el('taskListPane').style.display = tab === 'list' ? '' : 'none';
|
||||
el('taskConfigPane').style.display = tab === 'config' ? '' : 'none';
|
||||
el('taskRunPane').style.display = tab === 'runs' ? '' : 'none';
|
||||
el('taskTabList').className = 'btn-small' + (tab === 'list' ? ' btn-primary' : '');
|
||||
el('taskTabConfig').className = 'btn-small' + (tab === 'config' ? ' btn-primary' : '');
|
||||
}
|
||||
|
||||
function statusBadge(status) {
|
||||
var colors = {
|
||||
running: 'badge-info', completed: 'badge-success', failed: 'badge-danger',
|
||||
budget_exceeded: 'badge-warning', cancelled: 'badge-muted'
|
||||
};
|
||||
return '<span class="badge ' + (colors[status] || '') + '">' + status + '</span>';
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
if (!s) return '';
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
async function loadTasks() {
|
||||
try {
|
||||
var resp = await App.api.get('/api/v1/admin/tasks');
|
||||
var tasks = resp.data || [];
|
||||
var list = document.getElementById('taskAdminList');
|
||||
document.getElementById('taskCount').textContent = tasks.length;
|
||||
|
||||
if (!tasks.length) {
|
||||
list.innerHTML = '<div class="admin-empty">No scheduled tasks</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = tasks.map(function(t) {
|
||||
var active = t.is_active
|
||||
? '<span class="badge badge-success">active</span>'
|
||||
: '<span class="badge badge-muted">paused</span>';
|
||||
var scope = '<span class="badge">' + t.scope + '</span>';
|
||||
var type = '<span class="badge">' + t.task_type + '</span>';
|
||||
var schedule = t.schedule === 'once' ? 'One-shot' : t.schedule;
|
||||
var nextRun = t.next_run_at ? new Date(t.next_run_at).toLocaleString() : '\u2014';
|
||||
var lastRun = t.last_run_at ? new Date(t.last_run_at).toLocaleString() : 'never';
|
||||
|
||||
return '<div class="admin-row" style="display:flex;align-items:center;gap:12px;padding:10px 12px">' +
|
||||
'<div style="flex:2">' +
|
||||
'<div style="font-weight:600;font-size:13px">' + escapeHtml(t.name) + '</div>' +
|
||||
'<div style="font-size:11px;color:var(--text-secondary);margin-top:2px">' +
|
||||
escapeHtml(schedule) + ' \u00b7 ' + t.run_count + ' runs \u00b7 last: ' + lastRun +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div style="display:flex;gap:6px;align-items:center">' + type + scope + active + '</div>' +
|
||||
'<div style="font-size:11px;color:var(--text-secondary);min-width:140px;text-align:right">next: ' + nextRun + '</div>' +
|
||||
'<div style="display:flex;gap:4px">' +
|
||||
'<button class="btn-small task-run-btn" data-id="' + t.id + '" title="Run Now">\u25b6</button>' +
|
||||
'<button class="btn-small task-history-btn" data-id="' + t.id + '" data-name="' + escapeHtml(t.name) + '" title="History">\ud83d\udccb</button>' +
|
||||
'<button class="btn-small btn-danger task-delete-btn" data-id="' + t.id + '" title="Delete">\u2715</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
list.querySelectorAll('.task-run-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function(e) { e.stopPropagation(); triggerRun(btn.dataset.id); });
|
||||
});
|
||||
list.querySelectorAll('.task-history-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function(e) { e.stopPropagation(); showRuns(btn.dataset.id, btn.dataset.name); });
|
||||
});
|
||||
list.querySelectorAll('.task-delete-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function(e) { e.stopPropagation(); deleteTask(btn.dataset.id); });
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[task-admin] Failed to load tasks:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerRun(taskId) {
|
||||
try {
|
||||
await App.api.post('/api/v1/admin/tasks/' + taskId + '/run', {});
|
||||
App.showToast('Task scheduled for immediate execution', 'success');
|
||||
} catch (err) {
|
||||
App.showToast(err.message || 'Failed to trigger run', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTask(taskId) {
|
||||
if (!confirm('Delete this task and all run history?')) return;
|
||||
try {
|
||||
await App.api.delete('/api/v1/admin/tasks/' + taskId);
|
||||
App.showToast('Task deleted', 'success');
|
||||
loadTasks();
|
||||
} catch (err) {
|
||||
App.showToast(err.message || 'Failed to delete task', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
var _currentTaskId = null;
|
||||
|
||||
async function showRuns(taskId, taskName) {
|
||||
_currentTaskId = taskId;
|
||||
document.getElementById('taskRunTitle').textContent = 'Run History: ' + taskName;
|
||||
showTab('runs');
|
||||
try {
|
||||
var resp = await App.api.get('/api/v1/tasks/' + taskId + '/runs');
|
||||
var runs = resp.data || [];
|
||||
var list = document.getElementById('taskRunList');
|
||||
|
||||
if (!runs.length) {
|
||||
list.innerHTML = '<div class="admin-empty">No runs yet</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = runs.map(function(r) {
|
||||
var started = new Date(r.started_at).toLocaleString();
|
||||
var completed = r.completed_at ? new Date(r.completed_at).toLocaleString() : '\u2014';
|
||||
var duration = r.wall_clock ? r.wall_clock + 's' : '\u2014';
|
||||
var errLine = r.error
|
||||
? '<div style="font-size:11px;color:var(--danger);margin-top:2px">' + escapeHtml(r.error) + '</div>'
|
||||
: '';
|
||||
|
||||
return '<div class="admin-row" style="padding:10px 12px">' +
|
||||
'<div style="display:flex;align-items:center;gap:12px">' +
|
||||
'<div style="flex:1">' +
|
||||
'<div style="font-size:12px">' + started + ' \u2192 ' + completed + '</div>' +
|
||||
'<div style="font-size:11px;color:var(--text-secondary);margin-top:2px">' +
|
||||
'tokens: ' + r.tokens_used + ' \u00b7 tools: ' + r.tool_calls + ' \u00b7 wall: ' + duration +
|
||||
'</div>' +
|
||||
errLine +
|
||||
'</div>' +
|
||||
statusBadge(r.status) +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
} catch (err) {
|
||||
console.error('[task-admin] Failed to load runs:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function killRun() {
|
||||
if (!_currentTaskId) return;
|
||||
try {
|
||||
await App.api.post('/api/v1/admin/tasks/' + _currentTaskId + '/kill', {});
|
||||
App.showToast('Active run cancelled', 'success');
|
||||
showRuns(_currentTaskId, document.getElementById('taskRunTitle').textContent.replace('Run History: ', ''));
|
||||
} catch (err) {
|
||||
App.showToast(err.message || 'Failed to kill run', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
var resp = await App.api.get('/api/v1/admin/settings/tasks');
|
||||
var cfg = resp || {};
|
||||
document.getElementById('taskCfgEnabled').checked = cfg.enabled !== false;
|
||||
document.getElementById('taskCfgPersonal').checked = cfg.allow_personal !== false;
|
||||
document.getElementById('taskCfgMaxConcurrent').value = cfg.max_concurrent || 5;
|
||||
document.getElementById('taskCfgMaxTokens').value = cfg.default_max_tokens || 4096;
|
||||
document.getElementById('taskCfgMaxToolCalls').value = cfg.default_max_tool_calls || 10;
|
||||
document.getElementById('taskCfgMaxWallClock').value = cfg.default_max_wall_clock || 300;
|
||||
} catch (_) {
|
||||
document.getElementById('taskCfgEnabled').checked = true;
|
||||
document.getElementById('taskCfgPersonal').checked = true;
|
||||
document.getElementById('taskCfgMaxConcurrent').value = 5;
|
||||
document.getElementById('taskCfgMaxTokens').value = 4096;
|
||||
document.getElementById('taskCfgMaxToolCalls').value = 10;
|
||||
document.getElementById('taskCfgMaxWallClock').value = 300;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
try {
|
||||
var payload = {
|
||||
enabled: document.getElementById('taskCfgEnabled').checked,
|
||||
allow_personal: document.getElementById('taskCfgPersonal').checked,
|
||||
max_concurrent: parseInt(document.getElementById('taskCfgMaxConcurrent').value) || 5,
|
||||
default_max_tokens: parseInt(document.getElementById('taskCfgMaxTokens').value) || 4096,
|
||||
default_max_tool_calls: parseInt(document.getElementById('taskCfgMaxToolCalls').value) || 10,
|
||||
default_max_wall_clock: parseInt(document.getElementById('taskCfgMaxWallClock').value) || 300
|
||||
};
|
||||
await App.api.put('/api/v1/admin/settings/tasks', payload);
|
||||
App.showToast('Task configuration saved', 'success');
|
||||
} catch (err) {
|
||||
App.showToast(err.message || 'Failed to save config', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
window._loadAdminTasks = async function() {
|
||||
var container = document.getElementById('adminDynamic');
|
||||
if (!container) return;
|
||||
container.innerHTML = SCAFFOLD_HTML;
|
||||
|
||||
document.getElementById('taskTabList').addEventListener('click', function() { showTab('list'); loadTasks(); });
|
||||
document.getElementById('taskTabConfig').addEventListener('click', function() { showTab('config'); loadConfig(); });
|
||||
document.getElementById('taskRunBackBtn').addEventListener('click', function() { showTab('list'); });
|
||||
document.getElementById('taskKillBtn').addEventListener('click', killRun);
|
||||
document.getElementById('taskCfgSaveBtn').addEventListener('click', saveConfig);
|
||||
|
||||
await loadTasks();
|
||||
};
|
||||
})();
|
||||
@@ -8,7 +8,7 @@
|
||||
const ADMIN_SECTIONS = {
|
||||
people: ['users', 'teams', 'groups'],
|
||||
ai: ['providers', 'models', 'personas', 'roles', 'knowledgeBases', 'memory'],
|
||||
workflows: ['workflows'],
|
||||
workflows: ['workflows', 'tasks'],
|
||||
routing: ['health', 'routing', 'capabilities'],
|
||||
system: ['settings', 'storage', 'extensions', 'channels', 'surfaces'],
|
||||
monitoring: ['usage', 'audit', 'stats'],
|
||||
@@ -18,7 +18,7 @@ const ADMIN_LABELS = {
|
||||
users: 'Users', teams: 'Teams', groups: 'Groups',
|
||||
providers: 'Providers', models: 'Models', personas: 'Personas', roles: 'Roles', knowledgeBases: 'Knowledge', memory: 'Memory',
|
||||
health: 'Health', routing: 'Routing', capabilities: 'Capabilities',
|
||||
workflows: 'Workflows',
|
||||
workflows: 'Workflows', tasks: 'Tasks',
|
||||
settings: 'Settings', storage: 'Storage', extensions: 'Extensions', channels: 'Channels', surfaces: 'Surfaces',
|
||||
usage: 'Usage', audit: 'Audit', stats: 'Stats',
|
||||
};
|
||||
@@ -52,6 +52,7 @@ const ADMIN_LOADERS = {
|
||||
stats: () => UI.loadAdminStats(),
|
||||
channels: () => UI.loadAdminChannels(),
|
||||
workflows: () => typeof _loadAdminWorkflows === 'function' ? _loadAdminWorkflows() : null,
|
||||
tasks: () => typeof _loadAdminTasks === 'function' ? _loadAdminTasks() : null,
|
||||
};
|
||||
|
||||
// Find which category a section belongs to
|
||||
@@ -924,6 +925,8 @@ Object.assign(UI, {
|
||||
'persona.create': 'Create personas',
|
||||
'persona.manage': 'Edit / delete team personas',
|
||||
'workflow.create': 'Create workflows',
|
||||
'task.create': 'Create scheduled tasks',
|
||||
'task.admin': 'Manage all tasks + config',
|
||||
'admin.view': 'Read-only admin access',
|
||||
'token.unlimited': 'Bypass token budgets',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user