MeshDD-Bot/static/js/scheduler.js
ppfeiffer c443a9f26d feat(auth): Rolle Mitarbeiter + Einladungs-Workflow (closes #7)
- Rollensystem: Public → Mitarbeiter → Admin (Rolle user entfällt)
- DB-Migration: must_change_password-Spalte, user→mitarbeiter
- require_staff_api(): erlaubt mitarbeiter + admin
- POST /api/admin/invite: Einladung mit auto-generiertem Passwort + E-Mail
- POST /auth/change-password: Pflicht-Passwortwechsel
- Login: force_password_change-Redirect
- Sidebar: sidebar-staff für Scheduler/NINA/Einstellungen
- Scheduler/NINA: read-only für Mitarbeiter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 22:51:06 +01:00

232 lines
9.2 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;
if (!user || user.role !== 'admin') {
const btn = document.getElementById('btnAddJob');
btn.disabled = true;
btn.title = 'Nur Lesezugriff';
}
loadJobs();
} });
// 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 =>
`<span class="badge bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle me-1 mb-1" role="button" title="${escapeHtml(v.desc)}" data-var="${escapeHtml(v.key)}" style="cursor:pointer;font-size:.7rem">${escapeHtml(v.label)}</span>`
).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 = '<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;
}
const isAdmin = currentUser && currentUser.role === 'admin';
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';
const statusCell = isAdmin
? `<span class="${statusClass} cursor-pointer" role="button" onclick="toggleJob('${escapeHtml(job.name)}')"><i class="bi ${statusIcon} me-1"></i>${statusText}</span>`
: `<span class="${statusClass}"><i class="bi ${statusIcon} me-1"></i>${statusText}</span>`;
const actions = isAdmin
? `<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>`
: `<span class="badge bg-secondary-subtle text-secondary-emphasis" style="font-size:.65rem">Nur Lesezugriff</span>`;
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">${statusCell}</td>
<td class="text-end">${actions}</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);
}
});
connectWebSocket();