MeshDD-Bot/static/js/scheduler.js
ppfeiffer 0d6b26f4f8 feat: v0.5.0 - Benutzerverwaltung mit Session-Authentifizierung
Rollen-basiertes Zugriffsystem (public/user/admin), Registrierung mit
E-Mail-Verifikation, bcrypt Passwort-Hashing, Admin-Benutzerverwaltung.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 19:38:17 +01:00

249 lines
8.8 KiB
JavaScript

const jobsTable = document.getElementById('jobsTable');
const jobModal = new bootstrap.Modal(document.getElementById('jobModal'));
let currentUser = null;
let jobs = [];
let editMode = false;
// Auth check
fetch('/api/auth/me').then(r => r.ok ? r.json() : null).then(u => {
currentUser = u;
updateNavbar();
updateSidebar();
});
function updateNavbar() {
if (currentUser) {
document.getElementById('userName').textContent = currentUser.name;
document.getElementById('userMenu').classList.remove('d-none');
document.getElementById('loginBtn').classList.add('d-none');
} else {
document.getElementById('userMenu').classList.add('d-none');
document.getElementById('loginBtn').classList.remove('d-none');
}
}
function updateSidebar() {
const isAdmin = currentUser && currentUser.role === 'admin';
document.querySelectorAll('.sidebar-admin').forEach(el => {
el.style.display = isAdmin ? '' : 'none';
});
}
// 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);
}
});
// Sidebar toggle (mobile)
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const sidebarBackdrop = document.getElementById('sidebarBackdrop');
if (sidebarToggle) {
sidebarToggle.addEventListener('click', () => sidebar.classList.toggle('open'));
sidebarBackdrop.addEventListener('click', () => sidebar.classList.remove('open'));
}
loadJobs();
connectWebSocket();