const jobsTable = document.getElementById('jobsTable');
const jobModal = new bootstrap.Modal(document.getElementById('jobModal'));
let currentUser = null;
let jobs = [];
let editMode = false;
initPage({ onAuth: (user) => { currentUser = user; } });
// Template variables available in message jobs
const MSG_VARS = [
{ key: '{time}', label: '{time}', desc: 'Uhrzeit (HH:MM)' },
{ key: '{date}', label: '{date}', desc: 'Datum (TT.MM.JJJJ)' },
{ key: '{datetime}', label: '{datetime}', desc: 'Datum + Uhrzeit' },
{ key: '{weekday}', label: '{weekday}', desc: 'Wochentag (Montag…)' },
{ key: '{nodes}', label: '{nodes}', desc: 'Anzahl Nodes gesamt' },
{ key: '{nodes_24h}', label: '{nodes_24h}', desc: 'Aktive Nodes (24h)' },
{ key: '{nodes_online}', label: '{nodes_online}', desc: 'Nodes online (< 15 Min)' },
{ key: '{version}', label: '{version}', desc: 'Bot-Version' },
];
// Type toggle label + variable hints
function updateCommandLabel() {
const isMsg = document.getElementById('jobType').value === 'message';
document.getElementById('jobCommandLabel').textContent = isMsg ? 'Nachricht' : 'Kommando';
document.getElementById('jobCommand').placeholder = isMsg ? 'Hallo Mesh! Heute ist {weekday}, {time} Uhr.' : '/weather';
const hints = document.getElementById('varHints');
const badges = document.getElementById('varBadges');
if (isMsg) {
badges.innerHTML = MSG_VARS.map(v =>
`${escapeHtml(v.label)}`
).join('');
hints.classList.remove('d-none');
} else {
hints.classList.add('d-none');
}
}
document.getElementById('varBadges').addEventListener('click', (e) => {
const badge = e.target.closest('[data-var]');
if (!badge) return;
const input = document.getElementById('jobCommand');
const start = input.selectionStart;
const end = input.selectionEnd;
const val = input.value;
input.value = val.slice(0, start) + badge.dataset.var + val.slice(end);
const pos = start + badge.dataset.var.length;
input.setSelectionRange(pos, pos);
input.focus();
});
document.getElementById('jobType').addEventListener('change', updateCommandLabel);
// WebSocket for live updates
function connectWebSocket() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${proto}//${location.host}/ws`);
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'scheduler_update') {
jobs = msg.data;
renderJobs();
}
};
ws.onclose = () => {
setTimeout(connectWebSocket, 3000);
};
ws.onerror = () => { ws.close(); };
}
// Load jobs
async function loadJobs() {
try {
const resp = await fetch('/api/scheduler/jobs');
jobs = await resp.json();
renderJobs();
} catch (e) {
jobsTable.innerHTML = '
| Fehler beim Laden |
';
}
}
function renderJobs() {
if (jobs.length === 0) {
jobsTable.innerHTML = '| Keine Jobs konfiguriert |
';
return;
}
jobsTable.innerHTML = jobs.map(job => {
const statusClass = job.enabled ? 'text-success' : 'text-body-secondary';
const statusIcon = job.enabled ? 'bi-check-circle-fill' : 'bi-pause-circle';
const statusText = job.enabled ? 'Aktiv' : 'Inaktiv';
const jobType = job.type === 'message' ? 'Nachricht' : 'Kommando';
const typeBadge = job.type === 'message' ? 'bg-warning text-dark' : 'bg-info text-white';
return `
| ${escapeHtml(job.name || '')} |
${escapeHtml(job.description || '')} |
${jobType} |
${escapeHtml(job.command || '')} |
${escapeHtml(job.cron || '')} |
${job.channel != null ? job.channel : 0} |
${statusText}
|
|
`;
}).join('');
}
async function toggleJob(name) {
const job = jobs.find(j => j.name === name);
if (!job) return;
try {
const resp = await fetch(`/api/scheduler/jobs/${encodeURIComponent(name)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: !job.enabled })
});
if (resp.ok) {
jobs = await resp.json();
renderJobs();
}
} catch (e) {
console.error('Toggle failed:', e);
}
}
function editJob(name) {
const job = jobs.find(j => j.name === name);
if (!job) return;
editMode = true;
document.getElementById('jobModalTitle').textContent = 'Job bearbeiten';
document.getElementById('jobOriginalName').value = job.name;
document.getElementById('jobName').value = job.name;
document.getElementById('jobDescription').value = job.description || '';
document.getElementById('jobType').value = job.type || 'command';
updateCommandLabel();
document.getElementById('jobCommand').value = job.command || '';
document.getElementById('jobCron').value = job.cron || '';
document.getElementById('jobChannel').value = job.channel != null ? job.channel : 0;
document.getElementById('jobEnabled').checked = !!job.enabled;
jobModal.show();
}
async function deleteJob(name) {
if (!confirm(`Job "${name}" wirklich loeschen?`)) return;
try {
const resp = await fetch(`/api/scheduler/jobs/${encodeURIComponent(name)}`, {
method: 'DELETE'
});
if (resp.ok) {
jobs = await resp.json();
renderJobs();
}
} catch (e) {
console.error('Delete failed:', e);
}
}
// New job button
document.getElementById('btnAddJob').addEventListener('click', () => {
editMode = false;
document.getElementById('jobModalTitle').textContent = 'Neuer Job';
document.getElementById('jobOriginalName').value = '';
document.getElementById('jobForm').reset();
document.getElementById('jobType').value = 'command';
updateCommandLabel();
document.getElementById('jobChannel').value = 0;
jobModal.show();
});
// Save job
document.getElementById('btnSaveJob').addEventListener('click', async () => {
const form = document.getElementById('jobForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const jobData = {
name: document.getElementById('jobName').value.trim(),
description: document.getElementById('jobDescription').value.trim(),
type: document.getElementById('jobType').value,
command: document.getElementById('jobCommand').value.trim(),
cron: document.getElementById('jobCron').value.trim(),
channel: parseInt(document.getElementById('jobChannel').value) || 0,
enabled: document.getElementById('jobEnabled').checked
};
try {
let resp;
if (editMode) {
const originalName = document.getElementById('jobOriginalName').value;
resp = await fetch(`/api/scheduler/jobs/${encodeURIComponent(originalName)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(jobData)
});
} else {
resp = await fetch('/api/scheduler/jobs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(jobData)
});
}
if (resp.ok) {
jobs = await resp.json();
renderJobs();
jobModal.hide();
}
} catch (e) {
console.error('Save failed:', e);
}
});
loadJobs();
connectWebSocket();