Changeset 0.27.2 (#169)

This commit is contained in:
2026-03-11 00:22:02 +00:00
parent e4efe6b934
commit dcb915555e
20 changed files with 2106 additions and 880 deletions

251
src/js/task-admin.js Normal file
View 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">&larr; 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
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();
};
})();

View File

@@ -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',
};