MeshDD-Bot/static/js/scheduler.js
ppfeiffer f2c6ba8e62 feat: v0.6.7 - Dashboard-Upgrade: Tabler-Theme, Charts, Kanalfilter, Node-Suche
- Tabler 1.4.0 als Admin-Theme: Bootstrap CSS/JS in allen 6 HTML-Seiten ersetzt
- style.css komplett ueberarbeitet: Inter-Font, Tabler CSS-Variablen, Schatten,
  verfeinerte Sidebar (Rounded Active-Links), Hover-Animation auf Info-Boxen,
  pulsierender Status-Dot
- app.js als shared Modul: Duplikation in allen JS-Dateien eliminiert (initPage,
  applyTheme, escapeHtml, Sidebar-Injektion)
- WebSocket Auth-Fix: Nachrichten nur noch an eingeloggte Clients (auth_clients)
- Bot-Uptime + Meshtastic-Verbindungsstatus in Dashboard und Stats-API
- Dark Mode Kartentiles: CartoDB Dark Matter fuer Karte + Node-Modal
- 3 Charts: Kanal-Anfragen (Doughnut), Hop-Verteilung (Bar), Hardware Top 5
- Nodes-Tabelle: Suchfeld, Online-Filter, sortierbare Spalten
- Nachrichten Kanalfilter: Filter-Buttons im Nachrichten-Card-Header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-17 22:43:35 +01:00

193 lines
7 KiB
JavaScript

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; } });
// 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 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();