update for app.js

This commit is contained in:
oberon 2026-02-16 11:24:23 +01:00
parent b8b2aec67d
commit d081477d63
11 changed files with 432 additions and 449 deletions

21
app.py
View File

@ -42,16 +42,20 @@ def cleanup_old_log_dirs(max_age_days=90):
logger.error(f"Fehler beim Löschen von {subdir_path}: {e}")
# ── Hilfsfunktion - globale Sounds laden ────────────────────────────────────────────────────────────────
def load_global_sounds():
global_sounds_path = os.path.join(app.root_path, 'sounds.json') # oder configs/sounds.json
if os.path.exists(global_sounds_path):
def load_default_sounds():
path = os.path.join(app.config['CONFIG_DIR'], 'default_sounds.json')
if os.path.exists(path):
try:
with open(global_sounds_path, 'r', encoding='utf-8') as f:
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data.get('global_sounds', [])
# sounds = data.get('global_sounds', data.get('sounds', [])) # flexibel
sounds = data.get('global_sounds', [])
logger.info(f"Globale Default-Sounds geladen: {len(sounds)} Einträge")
return sounds
except Exception as e:
logger.error(f"Fehler beim Laden globaler Sounds: {e}")
logger.error(f"Fehler beim Laden default_sounds.json: {e}")
return []
logger.info("Keine default_sounds.json gefunden")
return []
# ── Bluetooth ────────────────────────────────────────────────────────────────
@ -164,7 +168,7 @@ logger.info("Background-Monitor für Hub-Verbindung gestartet")
def load_configs():
configs = []
for filename in os.listdir(app.config['CONFIG_DIR']):
if filename.lower().endswith('.json'):
if filename.lower().endswith('.json') and filename != 'default_sounds.json':
path = os.path.join(app.config['CONFIG_DIR'], filename)
try:
with open(path, 'r', encoding='utf-8') as f:
@ -234,7 +238,8 @@ def control_page():
if current_config is None:
logger.warning("current_config ist None → Redirect zu index")
return redirect(url_for('index'))
# Globale Sounds immer laden
global_sounds = load_global_sounds()
logger.info(f"Übergebe config an Template: {current_config}")

View File

@ -1,4 +1,5 @@
{
"name": "Standard-Sounds (global)",
"global_sounds": [
{
"id": "dampflok-fahrt",

View File

@ -1,439 +1,21 @@
// static/js/app.js MK Control Frontend
// static/js/app.js MK Control Frontend Haupt-Einstieg
document.addEventListener('DOMContentLoaded', () => {
console.log('MK Control Frontend geladen');
console.log('MK Control Frontend gestartet');
// ── 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');
// Module laden (in dieser Reihenfolge wichtig!)
initConfig(); // config & globalSounds bereitstellen
initStatus(); // Status-Badge & Reconnect
initConnect(); // Connect- & Reconnect-Button
initChannels(); // Kanäle rendern + Listener
initSoundboard(); // Soundboard-Buttons + Volume
initConnectionCheck(); // Hintergrund-Status-Polling
if (!connectBtn) {
console.warn('Nicht auf der Steuerseite Connect-Button nicht gefunden');
// Dennoch Status und Soundboard-Logik initialisieren (falls vorhanden)
}
// ── Config & globale Sounds aus Template ──────────────────────────────────
const config = window.mkConfig || {};
const globalSounds = window.mkGlobalSounds || [];
console.log("app.js verwendet config:", config);
console.log("app.js verwendet globalSounds:", 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
// Initialen Status setzen
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 ────────────────────────────────────────────────────────
if (connectBtn) {
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 ──────────────────────────────────────
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);
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;
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);
}
});
// Status-Check nur starten, wenn auf Steuerseite
if (document.getElementById('connect-btn')) {
// startConnectionCheck(); // wird jetzt von connect/reconnect aufgerufen
}
});

16
static/js/config.js Normal file
View File

@ -0,0 +1,16 @@
// static/js/config.js Konfiguration & globale Daten
let config = {};
let globalSounds = [];
function initConfig() {
// Diese Werte werden in control.html per Jinja gesetzt
config = window.mkConfig || {};
globalSounds = window.mkGlobalSounds || [];
console.log("config.js → Config geladen:", config);
console.log("config.js → Globale Sounds:", globalSounds);
}
// Export (für andere Module)
window.initConfig = initConfig;

View File

@ -0,0 +1,33 @@
// static/js/connection-check.js
let connectionCheckInterval = null;
let failedChecks = 0;
const MAX_FAILED_CHECKS = 3;
function startConnectionCheck() {
if (connectionCheckInterval) clearInterval(connectionCheckInterval);
connectionCheckInterval = setInterval(async () => {
try {
const res = await fetch('/api/status');
const data = await res.json();
if (data.connected) {
failedChecks = 0;
updateStatus(true);
} else {
failedChecks++;
if (failedChecks >= MAX_FAILED_CHECKS) {
updateStatus(false, data.message || 'Mehrere Checks fehlgeschlagen');
}
}
} catch (err) {
failedChecks++;
if (failedChecks >= MAX_FAILED_CHECKS) {
updateStatus(false, 'Keine Antwort');
}
}
}, 6000);
}
window.startConnectionCheck = startConnectionCheck;

89
static/js/ui-channels.js Normal file
View File

@ -0,0 +1,89 @@
// static/js/ui-channels.js Kanal-Rendering & Listener
function renderChannels() {
console.log("renderChannels() START");
console.log("→ config.channels existiert?", !!config?.channels);
console.log("→ channels.length:", config?.channels?.length ?? "undefined");
const container = document.getElementById('channels-container');
if (!container) {
console.error("channelsContainer nicht gefunden!");
return;
}
container.innerHTML = '';
if (!config?.channels || config.channels.length === 0) {
container.innerHTML = '<p class="text-center text-muted py-5">Keine Kanäle definiert.</p>';
return;
}
config.channels.forEach(channel => {
const col = document.createElement('div');
col.className = 'col-md-6 mb-4';
let html = '';
if (channel.type === 'motor') {
html = `
<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 {
html = `
<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">${html}</div></div>`;
container.appendChild(col);
});
// Listener hinzufügen (wie bisher)
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} %`;
await sendControl(slider.dataset.port, value);
});
});
document.querySelectorAll('.stop-channel-btn').forEach(btn => {
btn.addEventListener('click', async () => {
await sendControl(btn.dataset.port, 0);
const slider = document.querySelector(`input[data-port="${btn.dataset.port}"]`);
if (slider) {
slider.value = 0;
const display = slider.parentElement.querySelector('.value-display');
if (display) display.textContent = '0 %';
}
});
});
document.querySelectorAll('.toggle-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const state = btn.dataset.state === 'off' ? 'on' : 'off';
btn.dataset.state = state;
btn.textContent = state === 'on' ? 'EIN' : 'AUS';
btn.classList.toggle('btn-success', state === 'on');
btn.classList.toggle('btn-outline-secondary', state === 'off');
await sendControl(btn.dataset.port, state === 'on' ? 1 : 0);
});
});
}
window.renderChannels = renderChannels;

72
static/js/ui-connect.js Normal file
View File

@ -0,0 +1,72 @@
// static/js/ui-connect.js Connect- & Reconnect-Logik
function initConnect() {
const connectBtn = document.getElementById('connect-btn');
const reconnectBtn = document.getElementById('reconnect-btn');
if (connectBtn) {
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 res = await fetch('/api/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
const data = await res.json();
if (data.success) {
document.getElementById('connect-section').style.display = 'none';
document.getElementById('control-section').style.display = 'block';
document.getElementById('reconnect-section').style.display = 'none';
updateStatus(true);
startConnectionCheck();
renderChannels();
console.log("→ Verbunden");
} else {
alert('Verbindungsfehler: ' + (data.message || 'Unbekannt'));
updateStatus(false);
}
} catch (err) {
console.error("Connect-Fehler:", err);
alert('Netzwerkfehler');
updateStatus(false);
} finally {
connectBtn.disabled = false;
connectBtn.innerHTML = '<i class="bi bi-bluetooth me-2"></i> Mit Hub 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 res = await fetch('/api/reconnect', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
const data = await res.json();
if (data.success) {
document.getElementById('reconnect-section').style.display = 'none';
document.getElementById('control-section').style.display = 'block';
updateStatus(true);
startConnectionCheck();
alert('Verbindung wiederhergestellt!');
} else {
alert('Re-Connect fehlgeschlagen: ' + (data.message || 'Unbekannt'));
}
} catch (err) {
console.error("Reconnect-Fehler:", err);
alert('Netzwerkfehler');
} finally {
reconnectBtn.disabled = false;
reconnectBtn.innerHTML = '<i class="bi bi-arrow-repeat me-2"></i> Erneut verbinden';
}
});
}
}
window.initConnect = initConnect;

135
static/js/ui-soundboard.js Normal file
View File

@ -0,0 +1,135 @@
// static/js/ui-soundboard.js
// Verantwortlich für: Soundboard-Buttons, Play/Stop, Volume-Regler
let currentSoundButton = null; // welcher Button gerade spielt (für optisches Feedback)
// Initialisierung wird von app.js aufgerufen
function initSoundboard() {
console.log('ui-soundboard.js → Initialisierung');
// ── Volume-Regler ──────────────────────────────────────────────────────────
const volumeSlider = document.getElementById('volume-slider');
const volumeDisplay = document.getElementById('volume-display');
if (volumeSlider && volumeDisplay) {
// Startwert anzeigen
volumeDisplay.textContent = `${volumeSlider.value} %`;
volumeSlider.addEventListener('input', () => {
const vol = parseInt(volumeSlider.value) / 100;
volumeDisplay.textContent = `${volumeSlider.value} %`;
// Lautstärke an Server senden
fetch('/api/set_volume', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ volume: vol })
})
.then(res => {
if (!res.ok) throw new Error('Volume-Set-Fehler');
console.log(`Volume auf ${vol} gesetzt`);
})
.catch(err => {
console.error('Volume-Set-Fehler:', err);
alert('Lautstärke konnte nicht gesetzt werden');
});
});
} else {
console.warn('Volume-Regler nicht gefunden im DOM');
}
// ── Play-Buttons ───────────────────────────────────────────────────────────
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';
currentSoundButton.disabled = false;
}
// Neuen Button markieren
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;
// Optional: Automatisch nach 35 Sekunden zurücksetzen
setTimeout(() => {
if (currentSoundButton === btn) {
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-primary');
btn.innerHTML = originalText;
btn.disabled = false;
}
}, 4000); // 4 Sekunden anpassbar
} else {
alert('Fehler beim Abspielen:\n' + (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',
headers: { 'Content-Type': 'application/json' }
});
const data = await res.json();
if (data.success) {
// Visuelles Feedback: aktuellen Play-Button zurücksetzen
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('Aktueller Sound gestoppt');
// Optional: kurzes Feedback am Button
btn.classList.add('btn-danger');
setTimeout(() => btn.classList.remove('btn-danger'), 800);
} else {
alert('Fehler beim Stoppen: ' + (data.message || 'Unbekannt'));
}
} catch (err) {
console.error('Stop-Sound-Fehler:', err);
alert('Netzwerkfehler beim Stoppen');
}
});
});
}
// Export für app.js
window.initSoundboard = initSoundboard;

30
static/js/ui-status.js Normal file
View File

@ -0,0 +1,30 @@
// static/js/ui-status.js Status-Badge & Reconnect
function updateStatus(connected, message = '') {
const badge = document.getElementById('connection-status');
const reconnect = document.getElementById('reconnect-section');
if (!badge) return;
if (connected) {
badge.className = 'badge bg-success px-3 py-2 fs-6';
badge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Verbunden' + (message ? ` ${message}` : '');
if (reconnect) reconnect.style.display = 'none';
} else {
badge.className = 'badge bg-danger px-3 py-2 fs-6';
badge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Getrennt' + (message ? ` ${message}` : '');
if (reconnect) reconnect.style.display = 'block';
}
}
function showReconnect() {
const control = document.getElementById('control-section');
const reconnect = document.getElementById('reconnect-section');
if (control && reconnect) {
control.style.display = 'none';
reconnect.style.display = 'block';
}
}
window.updateStatus = updateStatus;
window.showReconnect = showReconnect;

View File

@ -86,9 +86,18 @@
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<!-- Eigenes JS -->
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
<!-- Globale JavaScript-Module (immer laden) -->
<script src="{{ url_for('static', filename='js/config.js') }}"></script>
<script src="{{ url_for('static', filename='js/ui-status.js') }}"></script>
<script src="{{ url_for('static', filename='js/ui-connect.js') }}"></script>
<script src="{{ url_for('static', filename='js/ui-channels.js') }}"></script>
<script src="{{ url_for('static', filename='js/ui-soundboard.js') }}"></script>
<script src="{{ url_for('static', filename='js/connection-check.js') }}"></script>
{% block scripts %}{% endblock %}
<!-- Einstiegspunkt lädt und initialisiert alles -->
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
<!-- Seiten-spezifische Config setzen (nur wenn config existiert) -->
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -165,13 +165,24 @@
{% endblock %}
<!-- Seiten-spezifische Config setzen (nur wenn config existiert) -->
{% block scripts %}
<script>
// Config + globale Sounds global verfügbar machen
window.mkConfig = {{ config | tojson | safe }};
window.mkGlobalSounds = {{ global_sounds | tojson | safe }};
{% if config %}
<script>
// Config + globale Sounds global verfügbar machen (nur auf Seiten mit config)
window.mkConfig = {{ config | tojson | safe }};
window.mkGlobalSounds = {{ global_sounds | tojson | safe }};
console.log("window.mkConfig gesetzt:", window.mkConfig);
console.log("window.mkGlobalSounds:", window.mkGlobalSounds);
</script>
console.log("base.html → mkConfig gesetzt:", window.mkConfig);
console.log("base.html → mkGlobalSounds:", window.mkGlobalSounds);
// Module initialisieren (kann auch in app.js passieren, hier nur für Sicherheit)
initConfig();
initStatus();
initConnect();
initChannels();
initSoundboard();
// connection-check wird von connect/reconnect aufgerufen
</script>
{% endif %}
{% endblock %}