Add message sending from web dashboard with channel selector, new POST /api/send endpoint, scheduler support for free-text messages (type field), and modernized dashboard layout with glassmorphism navbar, gradient stat cards, chat bubbles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
213 lines
7.6 KiB
JavaScript
213 lines
7.6 KiB
JavaScript
const jobsTable = document.getElementById('jobsTable');
|
|
const jobModal = new bootstrap.Modal(document.getElementById('jobModal'));
|
|
let jobs = [];
|
|
let editMode = false;
|
|
|
|
// Theme toggle
|
|
const themeToggle = document.getElementById('themeToggle');
|
|
const themeIcon = document.getElementById('themeIcon');
|
|
|
|
function applyTheme(theme) {
|
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
|
themeIcon.className = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill';
|
|
localStorage.setItem('theme', theme);
|
|
}
|
|
|
|
applyTheme(localStorage.getItem('theme') || 'dark');
|
|
|
|
themeToggle.addEventListener('click', () => {
|
|
const current = document.documentElement.getAttribute('data-bs-theme');
|
|
applyTheme(current === 'dark' ? 'light' : 'dark');
|
|
});
|
|
|
|
// Type toggle label
|
|
function updateCommandLabel() {
|
|
const isMsg = document.getElementById('jobType').value === 'message';
|
|
document.getElementById('jobCommandLabel').textContent = isMsg ? 'Nachricht' : 'Kommando';
|
|
document.getElementById('jobCommand').placeholder = isMsg ? 'Hallo Mesh!' : '/weather';
|
|
}
|
|
|
|
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 = '<tr><td colspan="7" class="text-center text-danger py-3">Fehler beim Laden</td></tr>';
|
|
}
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function renderJobs() {
|
|
if (jobs.length === 0) {
|
|
jobsTable.innerHTML = '<tr><td colspan="8" class="text-center text-body-secondary py-3">Keine Jobs konfiguriert</td></tr>';
|
|
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' : 'bg-info';
|
|
return `<tr>
|
|
<td class="fw-semibold">${escapeHtml(job.name || '')}</td>
|
|
<td class="text-body-secondary">${escapeHtml(job.description || '')}</td>
|
|
<td><span class="badge ${typeBadge} bg-opacity-75">${jobType}</span></td>
|
|
<td><code>${escapeHtml(job.command || '')}</code></td>
|
|
<td><code>${escapeHtml(job.cron || '')}</code></td>
|
|
<td class="text-center">${job.channel != null ? job.channel : 0}</td>
|
|
<td class="text-center">
|
|
<span class="${statusClass} cursor-pointer" role="button" onclick="toggleJob('${escapeHtml(job.name)}')">
|
|
<i class="bi ${statusIcon} me-1"></i>${statusText}
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-outline-warning btn-sm py-0 px-1 me-1" onclick="editJob('${escapeHtml(job.name)}')" title="Bearbeiten">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<button class="btn btn-outline-danger btn-sm py-0 px-1" onclick="deleteJob('${escapeHtml(job.name)}')" title="Loeschen">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>`;
|
|
}).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();
|