440 lines
17 KiB
JavaScript
440 lines
17 KiB
JavaScript
// 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);
|
||
}
|
||
});
|
||
}
|
||
}); |