Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
394 lines
12 KiB
JavaScript
394 lines
12 KiB
JavaScript
// OpenFang Scheduler Page — Cron job management + event triggers unified view
|
|
'use strict';
|
|
|
|
function schedulerPage() {
|
|
return {
|
|
tab: 'jobs',
|
|
|
|
// -- Scheduled Jobs state --
|
|
jobs: [],
|
|
loading: true,
|
|
loadError: '',
|
|
|
|
// -- Event Triggers state --
|
|
triggers: [],
|
|
trigLoading: false,
|
|
trigLoadError: '',
|
|
|
|
// -- Run History state --
|
|
history: [],
|
|
historyLoading: false,
|
|
|
|
// -- Create Job form --
|
|
showCreateForm: false,
|
|
newJob: {
|
|
name: '',
|
|
cron: '',
|
|
agent_id: '',
|
|
message: '',
|
|
enabled: true
|
|
},
|
|
creating: false,
|
|
|
|
// -- Run Now state --
|
|
runningJobId: '',
|
|
|
|
// Cron presets
|
|
cronPresets: [
|
|
{ label: 'Every minute', cron: '* * * * *' },
|
|
{ label: 'Every 5 minutes', cron: '*/5 * * * *' },
|
|
{ label: 'Every 15 minutes', cron: '*/15 * * * *' },
|
|
{ label: 'Every 30 minutes', cron: '*/30 * * * *' },
|
|
{ label: 'Every hour', cron: '0 * * * *' },
|
|
{ label: 'Every 6 hours', cron: '0 */6 * * *' },
|
|
{ label: 'Daily at midnight', cron: '0 0 * * *' },
|
|
{ label: 'Daily at 9am', cron: '0 9 * * *' },
|
|
{ label: 'Weekdays at 9am', cron: '0 9 * * 1-5' },
|
|
{ label: 'Every Monday 9am', cron: '0 9 * * 1' },
|
|
{ label: 'First of month', cron: '0 0 1 * *' }
|
|
],
|
|
|
|
// ── Lifecycle ──
|
|
|
|
async loadData() {
|
|
this.loading = true;
|
|
this.loadError = '';
|
|
try {
|
|
await this.loadJobs();
|
|
} catch(e) {
|
|
this.loadError = e.message || 'Could not load scheduler data.';
|
|
}
|
|
this.loading = false;
|
|
},
|
|
|
|
async loadJobs() {
|
|
var data = await OpenFangAPI.get('/api/cron/jobs');
|
|
var raw = data.jobs || [];
|
|
// Normalize cron API response to flat fields the UI expects
|
|
this.jobs = raw.map(function(j) {
|
|
var cron = '';
|
|
if (j.schedule) {
|
|
if (j.schedule.kind === 'cron') cron = j.schedule.expr || '';
|
|
else if (j.schedule.kind === 'every') cron = 'every ' + j.schedule.every_secs + 's';
|
|
else if (j.schedule.kind === 'at') cron = 'at ' + (j.schedule.at || '');
|
|
}
|
|
return {
|
|
id: j.id,
|
|
name: j.name,
|
|
cron: cron,
|
|
agent_id: j.agent_id,
|
|
message: j.action ? j.action.message || '' : '',
|
|
enabled: j.enabled,
|
|
last_run: j.last_run,
|
|
next_run: j.next_run,
|
|
delivery: j.delivery ? j.delivery.kind || '' : '',
|
|
created_at: j.created_at
|
|
};
|
|
});
|
|
},
|
|
|
|
async loadTriggers() {
|
|
this.trigLoading = true;
|
|
this.trigLoadError = '';
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/triggers');
|
|
this.triggers = Array.isArray(data) ? data : [];
|
|
} catch(e) {
|
|
this.triggers = [];
|
|
this.trigLoadError = e.message || 'Could not load triggers.';
|
|
}
|
|
this.trigLoading = false;
|
|
},
|
|
|
|
async loadHistory() {
|
|
this.historyLoading = true;
|
|
try {
|
|
var historyItems = [];
|
|
var jobs = this.jobs || [];
|
|
for (var i = 0; i < jobs.length; i++) {
|
|
var job = jobs[i];
|
|
if (job.last_run) {
|
|
historyItems.push({
|
|
timestamp: job.last_run,
|
|
name: job.name || '(unnamed)',
|
|
type: 'schedule',
|
|
status: 'completed',
|
|
run_count: 0
|
|
});
|
|
}
|
|
}
|
|
var triggers = this.triggers || [];
|
|
for (var j = 0; j < triggers.length; j++) {
|
|
var t = triggers[j];
|
|
if (t.fire_count > 0) {
|
|
historyItems.push({
|
|
timestamp: t.created_at,
|
|
name: 'Trigger: ' + this.triggerType(t.pattern),
|
|
type: 'trigger',
|
|
status: 'fired',
|
|
run_count: t.fire_count
|
|
});
|
|
}
|
|
}
|
|
historyItems.sort(function(a, b) {
|
|
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
|
|
});
|
|
this.history = historyItems;
|
|
} catch(e) {
|
|
this.history = [];
|
|
}
|
|
this.historyLoading = false;
|
|
},
|
|
|
|
// ── Job CRUD ──
|
|
|
|
async createJob() {
|
|
if (!this.newJob.name.trim()) {
|
|
OpenFangToast.warn('Please enter a job name');
|
|
return;
|
|
}
|
|
if (!this.newJob.cron.trim()) {
|
|
OpenFangToast.warn('Please enter a cron expression');
|
|
return;
|
|
}
|
|
this.creating = true;
|
|
try {
|
|
var jobName = this.newJob.name;
|
|
var body = {
|
|
agent_id: this.newJob.agent_id,
|
|
name: this.newJob.name,
|
|
schedule: { kind: 'cron', expr: this.newJob.cron },
|
|
action: { kind: 'agent_turn', message: this.newJob.message || 'Scheduled task: ' + this.newJob.name },
|
|
delivery: { kind: 'last_channel' },
|
|
enabled: this.newJob.enabled
|
|
};
|
|
await OpenFangAPI.post('/api/cron/jobs', body);
|
|
this.showCreateForm = false;
|
|
this.newJob = { name: '', cron: '', agent_id: '', message: '', enabled: true };
|
|
OpenFangToast.success('Schedule "' + jobName + '" created');
|
|
await this.loadJobs();
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to create schedule: ' + (e.message || e));
|
|
}
|
|
this.creating = false;
|
|
},
|
|
|
|
async toggleJob(job) {
|
|
try {
|
|
var newState = !job.enabled;
|
|
await OpenFangAPI.put('/api/cron/jobs/' + job.id + '/enable', { enabled: newState });
|
|
job.enabled = newState;
|
|
OpenFangToast.success('Schedule ' + (newState ? 'enabled' : 'paused'));
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to toggle schedule: ' + (e.message || e));
|
|
}
|
|
},
|
|
|
|
deleteJob(job) {
|
|
var self = this;
|
|
var jobName = job.name || job.id;
|
|
OpenFangToast.confirm('Delete Schedule', 'Delete "' + jobName + '"? This cannot be undone.', async function() {
|
|
try {
|
|
await OpenFangAPI.del('/api/cron/jobs/' + job.id);
|
|
self.jobs = self.jobs.filter(function(j) { return j.id !== job.id; });
|
|
OpenFangToast.success('Schedule "' + jobName + '" deleted');
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to delete schedule: ' + (e.message || e));
|
|
}
|
|
});
|
|
},
|
|
|
|
async runNow(job) {
|
|
this.runningJobId = job.id;
|
|
try {
|
|
var result = await OpenFangAPI.post('/api/schedules/' + job.id + '/run', {});
|
|
if (result.status === 'completed') {
|
|
OpenFangToast.success('Schedule "' + (job.name || 'job') + '" executed successfully');
|
|
job.last_run = new Date().toISOString();
|
|
} else {
|
|
OpenFangToast.error('Schedule run failed: ' + (result.error || 'Unknown error'));
|
|
}
|
|
} catch(e) {
|
|
OpenFangToast.error('Run Now is not yet available for cron jobs');
|
|
}
|
|
this.runningJobId = '';
|
|
},
|
|
|
|
// ── Trigger helpers ──
|
|
|
|
triggerType(pattern) {
|
|
if (!pattern) return 'unknown';
|
|
if (typeof pattern === 'string') return pattern;
|
|
var keys = Object.keys(pattern);
|
|
if (keys.length === 0) return 'unknown';
|
|
var key = keys[0];
|
|
var names = {
|
|
lifecycle: 'Lifecycle',
|
|
agent_spawned: 'Agent Spawned',
|
|
agent_terminated: 'Agent Terminated',
|
|
system: 'System',
|
|
system_keyword: 'System Keyword',
|
|
memory_update: 'Memory Update',
|
|
memory_key_pattern: 'Memory Key',
|
|
all: 'All Events',
|
|
content_match: 'Content Match'
|
|
};
|
|
return names[key] || key.replace(/_/g, ' ');
|
|
},
|
|
|
|
async toggleTrigger(trigger) {
|
|
try {
|
|
var newState = !trigger.enabled;
|
|
await OpenFangAPI.put('/api/triggers/' + trigger.id, { enabled: newState });
|
|
trigger.enabled = newState;
|
|
OpenFangToast.success('Trigger ' + (newState ? 'enabled' : 'disabled'));
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to toggle trigger: ' + (e.message || e));
|
|
}
|
|
},
|
|
|
|
deleteTrigger(trigger) {
|
|
var self = this;
|
|
OpenFangToast.confirm('Delete Trigger', 'Delete this trigger? This cannot be undone.', async function() {
|
|
try {
|
|
await OpenFangAPI.del('/api/triggers/' + trigger.id);
|
|
self.triggers = self.triggers.filter(function(t) { return t.id !== trigger.id; });
|
|
OpenFangToast.success('Trigger deleted');
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to delete trigger: ' + (e.message || e));
|
|
}
|
|
});
|
|
},
|
|
|
|
// ── Utility ──
|
|
|
|
get availableAgents() {
|
|
return Alpine.store('app').agents || [];
|
|
},
|
|
|
|
agentName(agentId) {
|
|
if (!agentId) return '(any)';
|
|
var agents = this.availableAgents;
|
|
for (var i = 0; i < agents.length; i++) {
|
|
if (agents[i].id === agentId) return agents[i].name;
|
|
}
|
|
if (agentId.length > 12) return agentId.substring(0, 8) + '...';
|
|
return agentId;
|
|
},
|
|
|
|
describeCron(expr) {
|
|
if (!expr) return '';
|
|
// Handle non-cron schedule descriptions
|
|
if (expr.indexOf('every ') === 0) return expr;
|
|
if (expr.indexOf('at ') === 0) return 'One-time: ' + expr.substring(3);
|
|
|
|
var map = {
|
|
'* * * * *': 'Every minute',
|
|
'*/2 * * * *': 'Every 2 minutes',
|
|
'*/5 * * * *': 'Every 5 minutes',
|
|
'*/10 * * * *': 'Every 10 minutes',
|
|
'*/15 * * * *': 'Every 15 minutes',
|
|
'*/30 * * * *': 'Every 30 minutes',
|
|
'0 * * * *': 'Every hour',
|
|
'0 */2 * * *': 'Every 2 hours',
|
|
'0 */4 * * *': 'Every 4 hours',
|
|
'0 */6 * * *': 'Every 6 hours',
|
|
'0 */12 * * *': 'Every 12 hours',
|
|
'0 0 * * *': 'Daily at midnight',
|
|
'0 6 * * *': 'Daily at 6:00 AM',
|
|
'0 9 * * *': 'Daily at 9:00 AM',
|
|
'0 12 * * *': 'Daily at noon',
|
|
'0 18 * * *': 'Daily at 6:00 PM',
|
|
'0 9 * * 1-5': 'Weekdays at 9:00 AM',
|
|
'0 9 * * 1': 'Mondays at 9:00 AM',
|
|
'0 0 * * 0': 'Sundays at midnight',
|
|
'0 0 1 * *': '1st of every month',
|
|
'0 0 * * 1': 'Mondays at midnight'
|
|
};
|
|
if (map[expr]) return map[expr];
|
|
|
|
var parts = expr.split(' ');
|
|
if (parts.length !== 5) return expr;
|
|
|
|
var min = parts[0];
|
|
var hour = parts[1];
|
|
var dom = parts[2];
|
|
var mon = parts[3];
|
|
var dow = parts[4];
|
|
|
|
if (min.indexOf('*/') === 0 && hour === '*' && dom === '*' && mon === '*' && dow === '*') {
|
|
return 'Every ' + min.substring(2) + ' minutes';
|
|
}
|
|
if (min === '0' && hour.indexOf('*/') === 0 && dom === '*' && mon === '*' && dow === '*') {
|
|
return 'Every ' + hour.substring(2) + ' hours';
|
|
}
|
|
|
|
var dowNames = { '0': 'Sun', '1': 'Mon', '2': 'Tue', '3': 'Wed', '4': 'Thu', '5': 'Fri', '6': 'Sat', '7': 'Sun',
|
|
'1-5': 'Weekdays', '0,6': 'Weekends', '6,0': 'Weekends' };
|
|
|
|
if (dom === '*' && mon === '*' && min.match(/^\d+$/) && hour.match(/^\d+$/)) {
|
|
var h = parseInt(hour, 10);
|
|
var m = parseInt(min, 10);
|
|
var ampm = h >= 12 ? 'PM' : 'AM';
|
|
var h12 = h === 0 ? 12 : (h > 12 ? h - 12 : h);
|
|
var mStr = m < 10 ? '0' + m : '' + m;
|
|
var timeStr = h12 + ':' + mStr + ' ' + ampm;
|
|
if (dow === '*') return 'Daily at ' + timeStr;
|
|
var dowLabel = dowNames[dow] || ('DoW ' + dow);
|
|
return dowLabel + ' at ' + timeStr;
|
|
}
|
|
|
|
return expr;
|
|
},
|
|
|
|
applyCronPreset(preset) {
|
|
this.newJob.cron = preset.cron;
|
|
},
|
|
|
|
formatTime(ts) {
|
|
if (!ts) return '-';
|
|
try {
|
|
var d = new Date(ts);
|
|
if (isNaN(d.getTime())) return '-';
|
|
return d.toLocaleString();
|
|
} catch(e) { return '-'; }
|
|
},
|
|
|
|
relativeTime(ts) {
|
|
if (!ts) return 'never';
|
|
try {
|
|
var diff = Date.now() - new Date(ts).getTime();
|
|
if (isNaN(diff)) return 'never';
|
|
if (diff < 0) {
|
|
// Future time
|
|
var absDiff = Math.abs(diff);
|
|
if (absDiff < 60000) return 'in <1m';
|
|
if (absDiff < 3600000) return 'in ' + Math.floor(absDiff / 60000) + 'm';
|
|
if (absDiff < 86400000) return 'in ' + Math.floor(absDiff / 3600000) + 'h';
|
|
return 'in ' + Math.floor(absDiff / 86400000) + 'd';
|
|
}
|
|
if (diff < 60000) return 'just now';
|
|
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
|
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
|
|
return Math.floor(diff / 86400000) + 'd ago';
|
|
} catch(e) { return 'never'; }
|
|
},
|
|
|
|
jobCount() {
|
|
var enabled = 0;
|
|
for (var i = 0; i < this.jobs.length; i++) {
|
|
if (this.jobs[i].enabled) enabled++;
|
|
}
|
|
return enabled;
|
|
},
|
|
|
|
triggerCount() {
|
|
var enabled = 0;
|
|
for (var i = 0; i < this.triggers.length; i++) {
|
|
if (this.triggers[i].enabled) enabled++;
|
|
}
|
|
return enabled;
|
|
}
|
|
};
|
|
}
|