2026-02-15 22:03:15 +01:00

440 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// static/js/app.js MK Control Frontend
document.addEventListener('DOMContentLoaded', () => {
console.log('MK Control Frontend geladen');
// ── Elemente ───────────────────────────────────────────────────────────────
const connectBtn = document.getElementById('connect-btn');
const connectSection = document.getElementById('connect-section');
const controlSection = document.getElementById('control-section');
const channelsContainer = document.getElementById('channels-container');
const stopAllBtn = document.getElementById('stop-all-btn');
const reconnectSection = document.getElementById('reconnect-section');
const reconnectBtn = document.getElementById('reconnect-btn');
const statusBadge = document.getElementById('connection-status');
const volumeSlider = document.getElementById('volume-slider');
const volumeDisplay = document.getElementById('volume-display');
if (!connectBtn) {
console.warn('Nicht auf der Steuerseite Connect-Button nicht gefunden');
return;
}
// ── Config aus Template ────────────────────────────────────────────────────
const config = {{ config | tojson | safe }};
const globalSounds = {{ global_sounds | tojson | safe }};
window.mkConfig = config; // global für Konsistenz
console.log("Config im JS:", config);
console.log("Globale Sounds:", globalSounds);
// ── Status-Anzeige ─────────────────────────────────────────────────────────
function updateStatus(connected, message = '') {
if (!statusBadge) return;
if (connected) {
statusBadge.className = 'badge bg-success px-3 py-2 fs-6';
statusBadge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Verbunden' + (message ? ` ${message}` : '');
if (reconnectSection) reconnectSection.style.display = 'none';
} else {
statusBadge.className = 'badge bg-danger px-3 py-2 fs-6';
statusBadge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Getrennt' + (message ? ` ${message}` : '');
if (reconnectSection) reconnectSection.style.display = 'block';
}
}
// Initialer Status
updateStatus(false);
// ── Automatische Verbindungsprüfung ────────────────────────────────────────
let connectionCheckInterval = null;
let failedChecks = 0;
const MAX_FAILED_CHECKS = 3;
function startConnectionCheck() {
if (connectionCheckInterval) clearInterval(connectionCheckInterval);
connectionCheckInterval = setInterval(async () => {
try {
console.log("→ Status-Check ...");
const res = await fetch('/api/status');
const data = await res.json();
if (data.connected) {
failedChecks = 0;
updateStatus(true);
} else {
failedChecks++;
console.warn(`Status-Check fehlgeschlagen (${failedChecks}/${MAX_FAILED_CHECKS}):`, data.message);
if (failedChecks >= MAX_FAILED_CHECKS) {
updateStatus(false, data.message || 'Mehrere Checks fehlgeschlagen');
}
}
} catch (err) {
failedChecks++;
console.warn(`Status-Check Netzwerkfehler (${failedChecks}/${MAX_FAILED_CHECKS}):`, err);
if (failedChecks >= MAX_FAILED_CHECKS) {
updateStatus(false, 'Keine Antwort vom Hub/Server');
}
}
}, 6000); // 6 Sekunden
}
window.addEventListener('beforeunload', () => {
if (connectionCheckInterval) clearInterval(connectionCheckInterval);
});
// ── Connect-Button ────────────────────────────────────────────────────────
connectBtn.addEventListener('click', async () => {
connectBtn.disabled = true;
connectBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span> Verbinde...';
try {
console.log("→ Sende /api/connect ...");
const response = await fetch('/api/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
console.log("→ Antwort erhalten:", response.status);
const result = await response.json();
console.log("→ Resultat:", result);
if (result.success) {
console.log("→ Connect erfolgreich blende Sections um");
connectSection.style.display = 'none';
controlSection.style.display = 'block';
reconnectSection.style.display = 'none';
updateStatus(true);
startConnectionCheck();
console.log("→ Rufe renderChannels() auf");
renderChannels();
console.log("→ renderChannels() abgeschlossen");
} else {
console.warn("→ Connect fehlgeschlagen:", result.message);
alert('Verbindung fehlgeschlagen:\n' + (result.message || 'Unbekannter Fehler'));
updateStatus(false);
}
} catch (err) {
console.error("→ Connect-Fehler:", err);
alert('Netzwerk- oder Verbindungsfehler: ' + err.message);
updateStatus(false);
} finally {
connectBtn.disabled = false;
connectBtn.innerHTML = '<i class="bi bi-bluetooth me-2"></i> Mit Hub verbinden';
}
});
// ── Alle stoppen ──────────────────────────────────────────────────────────
if (stopAllBtn) {
stopAllBtn.addEventListener('click', async () => {
if (!confirm('Wirklich ALLE Kanäle stoppen?')) return;
try {
const res = await fetch('/api/stop_all', { method: 'POST' });
const data = await res.json();
if (data.success) {
// Alle Motor-Slider zurücksetzen
document.querySelectorAll('.motor-slider').forEach(slider => {
slider.value = 0;
const display = slider.parentElement.querySelector('.value-display');
if (display) display.textContent = '0 %';
});
console.log('Alle Kanäle gestoppt');
alert('Alle Kanäle gestoppt');
} else {
alert('Fehler beim Stoppen:\n' + (data.message || 'Unbekannt'));
}
} catch (err) {
console.error('Stop-all Fehler:', err);
alert('Netzwerkfehler beim Stoppen aller Kanäle');
}
});
}
// ── Erneut verbinden ──────────────────────────────────────────────────────
if (reconnectBtn) {
reconnectBtn.addEventListener('click', async () => {
reconnectBtn.disabled = true;
reconnectBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Verbinde...';
try {
const response = await fetch('/api/reconnect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (result.success) {
reconnectSection.style.display = 'none';
controlSection.style.display = 'block';
updateStatus(true);
startConnectionCheck();
alert('Verbindung wiederhergestellt!');
} else {
alert('Erneute Verbindung fehlgeschlagen:\n' + (result.message || 'Unbekannt'));
}
} catch (err) {
console.error('Reconnect-Fehler:', err);
alert('Netzwerkfehler beim erneuten Verbinden');
} finally {
reconnectBtn.disabled = false;
reconnectBtn.innerHTML = '<i class="bi bi-arrow-repeat me-2"></i> Erneut verbinden';
}
});
}
// ── Kanäle dynamisch rendern ──────────────────────────────────────────────
function renderChannels() {
console.log("renderChannels() START");
console.log("→ config.channels existiert?", !!config?.channels);
console.log("→ channels.length:", config?.channels?.length ?? "undefined");
if (!channelsContainer) {
console.error("→ channelsContainer nicht gefunden im DOM!");
return;
}
channelsContainer.innerHTML = '';
console.log("→ Container geleert");
if (!config?.channels || config.channels.length === 0) {
console.warn("→ Keine Kanäle erkannt zeige leere Meldung");
channelsContainer.innerHTML = '<p class="text-center text-muted py-5">Keine Kanäle in der Konfiguration definiert.</p>';
return;
}
console.log("→ Beginne mit Rendern von", config.channels.length, "Kanälen");
config.channels.forEach(channel => {
const col = document.createElement('div');
col.className = 'col-md-6 mb-4';
let controlHTML = '';
if (channel.type === 'motor') {
controlHTML = `
<label class="form-label fw-bold">${channel.name} (${channel.port})</label>
<input type="range" class="form-range motor-slider"
min="-100" max="100" value="0" step="5"
data-port="${channel.port}">
<div class="d-flex justify-content-between mt-1 small">
<span>-100 %</span>
<span class="value-display fw-bold">0 %</span>
<span>+100 %</span>
</div>
<div class="text-center mt-2">
<button class="btn btn-sm btn-outline-danger stop-channel-btn"
data-port="${channel.port}">
<i class="bi bi-stop-fill me-1"></i> Stop
</button>
</div>
`;
} else {
controlHTML = `
<label class="form-label fw-bold">${channel.name} (${channel.port})</label>
<button class="btn btn-outline-secondary w-100 toggle-btn"
data-port="${channel.port}" data-state="off">
AUS
</button>
`;
}
col.innerHTML = `
<div class="card h-100 shadow-sm">
<div class="card-body">
${controlHTML}
</div>
</div>
`;
channelsContainer.appendChild(col);
});
// ── Event-Listener Slider (Motoren) ─────────────────────────────────────
document.querySelectorAll('.motor-slider').forEach(slider => {
const display = slider.parentElement.querySelector('.value-display');
slider.addEventListener('input', async () => {
const value = parseInt(slider.value) / 100;
if (display) display.textContent = `${slider.value} %`;
try {
await sendControl(slider.dataset.port, value);
} catch (err) {
console.error('Slider-Steuerfehler:', err);
}
});
});
// ── Event-Listener Einzel-Stop pro Kanal ────────────────────────────────
document.querySelectorAll('.stop-channel-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const port = btn.dataset.port;
try {
await sendControl(port, 0);
// Slider zurücksetzen
const slider = document.querySelector(`input[data-port="${port}"]`);
if (slider) {
slider.value = 0;
const display = slider.parentElement.querySelector('.value-display');
if (display) display.textContent = '0 %';
}
// Feedback
btn.classList.add('btn-danger');
setTimeout(() => btn.classList.remove('btn-danger'), 600);
console.log(`Einzelstop: ${port}`);
} catch (err) {
console.error('Einzelstop-Fehler:', err);
}
});
});
// ── Event-Listener Toggle-Buttons (Licht/Sound/Fogger) ──────────────────
document.querySelectorAll('.toggle-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const current = btn.dataset.state;
const newState = current === 'off' ? 'on' : 'off';
btn.dataset.state = newState;
btn.textContent = newState === 'on' ? 'EIN' : 'AUS';
btn.classList.toggle('btn-success', newState === 'on');
btn.classList.toggle('btn-outline-secondary', newState === 'off');
try {
await sendControl(btn.dataset.port, newState === 'on' ? 1 : 0);
} catch (err) {
console.error('Toggle-Fehler:', err);
}
});
});
}
// ── Hilfsfunktion: Steuerbefehl senden ────────────────────────────────────
async function sendControl(port, value) {
try {
const res = await fetch('/api/control', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ port, value })
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.message || 'Steuerbefehl fehlgeschlagen');
}
} catch (err) {
console.error('sendControl Fehler:', err);
// Bei Fehler: Verbindungsprüfung triggern
updateStatus(false, 'Fehler beim Senden');
}
}
// ── Hilfsfunktion: Reconnect-Bereich anzeigen ─────────────────────────────
function showReconnect() {
if (reconnectSection && controlSection) {
controlSection.style.display = 'none';
reconnectSection.style.display = 'block';
}
}
// ── Soundboard Buttons ─────────────────────────────────────────────────────
let currentSoundButton = null;
document.querySelectorAll('.play-sound-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const soundId = btn.dataset.soundId;
// Alten Play-Button zurücksetzen
if (currentSoundButton && currentSoundButton !== btn) {
currentSoundButton.classList.remove('btn-success');
currentSoundButton.classList.add('btn-outline-primary', 'btn-outline-secondary');
currentSoundButton.innerHTML = currentSoundButton.dataset.originalText || '<i class="bi bi-play-fill me-2"></i> Abspielen';
}
btn.disabled = true;
const originalText = btn.innerHTML;
btn.dataset.originalText = originalText;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Spielt...';
try {
const res = await fetch('/api/play_sound', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sound_id: soundId })
});
const data = await res.json();
if (data.success) {
btn.classList.remove('btn-outline-primary', 'btn-outline-secondary');
btn.classList.add('btn-success');
btn.innerHTML = '<i class="bi bi-check-lg me-2"></i> Gespielt';
currentSoundButton = btn;
} else {
alert('Fehler: ' + (data.message || 'Unbekannt'));
btn.innerHTML = originalText;
btn.disabled = false;
}
} catch (err) {
console.error('Sound-Play-Fehler:', err);
alert('Netzwerkfehler beim Abspielen');
btn.innerHTML = originalText;
btn.disabled = false;
}
});
});
// ── Stop-Button für aktuellen Sound
document.querySelectorAll('.stop-sound-btn').forEach(btn => {
btn.addEventListener('click', async () => {
try {
const res = await fetch('/api/stop_sound', { method: 'POST' });
const data = await res.json();
if (data.success) {
if (currentSoundButton) {
currentSoundButton.classList.remove('btn-success');
currentSoundButton.classList.add('btn-outline-primary', 'btn-outline-secondary');
currentSoundButton.innerHTML = currentSoundButton.dataset.originalText || '<i class="bi bi-play-fill me-2"></i> Abspielen';
currentSoundButton.disabled = false;
currentSoundButton = null;
}
console.log('Sound gestoppt');
} else {
alert('Fehler: ' + data.message);
}
} catch (err) {
console.error('Stop-Sound-Fehler:', err);
alert('Netzwerkfehler beim Stoppen');
}
});
});
// ── Volume-Regler
if (volumeSlider && volumeDisplay) {
volumeSlider.addEventListener('input', () => {
const vol = volumeSlider.value / 100;
volumeDisplay.textContent = `${volumeSlider.value} %`;
try {
fetch('/api/set_volume', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ volume: vol })
}).catch(err => console.error('Volume-Set-Fehler:', err));
} catch (err) {
console.error('Volume-Fehler:', err);
}
});
}
});