diff --git a/app.py b/app.py
index 16b1678..398d319 100644
--- a/app.py
+++ b/app.py
@@ -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}")
diff --git a/configs/sounds.json b/configs/default_sounds.json
similarity index 92%
rename from configs/sounds.json
rename to configs/default_sounds.json
index 1412eef..7b0e993 100644
--- a/configs/sounds.json
+++ b/configs/default_sounds.json
@@ -1,4 +1,5 @@
{
+ "name": "Standard-Sounds (global)",
"global_sounds": [
{
"id": "dampflok-fahrt",
diff --git a/static/js/app.js b/static/js/app.js
index ad5b6e1..322dd22 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -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 = ' Verbunden' + (message ? ` – ${message}` : '');
- if (reconnectSection) reconnectSection.style.display = 'none';
- } else {
- statusBadge.className = 'badge bg-danger px-3 py-2 fs-6';
- statusBadge.innerHTML = ' 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 = ' 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 = ' 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 = ' 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 = ' 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 = '
Keine Kanäle in der Konfiguration definiert.
';
- 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 = `
-
-
-
- -100 %
- 0 %
- +100 %
-
-
-
-
- `;
- } else {
- controlHTML = `
-
-
- `;
- }
-
- col.innerHTML = `
-
- `;
-
- 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 || ' Abspielen';
- }
-
- btn.disabled = true;
- const originalText = btn.innerHTML;
- btn.dataset.originalText = originalText;
- btn.innerHTML = ' 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 = ' 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 || ' 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
}
});
\ No newline at end of file
diff --git a/static/js/config.js b/static/js/config.js
new file mode 100644
index 0000000..644cf75
--- /dev/null
+++ b/static/js/config.js
@@ -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;
\ No newline at end of file
diff --git a/static/js/connection-check.js b/static/js/connection-check.js
new file mode 100644
index 0000000..2f3cef7
--- /dev/null
+++ b/static/js/connection-check.js
@@ -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;
\ No newline at end of file
diff --git a/static/js/ui-channels.js b/static/js/ui-channels.js
new file mode 100644
index 0000000..84f73cd
--- /dev/null
+++ b/static/js/ui-channels.js
@@ -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 = 'Keine Kanäle definiert.
';
+ return;
+ }
+
+ config.channels.forEach(channel => {
+ const col = document.createElement('div');
+ col.className = 'col-md-6 mb-4';
+
+ let html = '';
+
+ if (channel.type === 'motor') {
+ html = `
+
+
+
+ -100 %
+ 0 %
+ +100 %
+
+
+
+
+ `;
+ } else {
+ html = `
+
+
+ `;
+ }
+
+ col.innerHTML = ``;
+ 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;
\ No newline at end of file
diff --git a/static/js/ui-connect.js b/static/js/ui-connect.js
new file mode 100644
index 0000000..50339c3
--- /dev/null
+++ b/static/js/ui-connect.js
@@ -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 = ' 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 = ' Mit Hub verbinden';
+ }
+ });
+ }
+
+ if (reconnectBtn) {
+ reconnectBtn.addEventListener('click', async () => {
+ reconnectBtn.disabled = true;
+ reconnectBtn.innerHTML = ' 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 = ' Erneut verbinden';
+ }
+ });
+ }
+}
+
+window.initConnect = initConnect;
\ No newline at end of file
diff --git a/static/js/ui-soundboard.js b/static/js/ui-soundboard.js
new file mode 100644
index 0000000..6387319
--- /dev/null
+++ b/static/js/ui-soundboard.js
@@ -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 || ' Abspielen';
+ currentSoundButton.disabled = false;
+ }
+
+ // Neuen Button markieren
+ btn.disabled = true;
+ const originalText = btn.innerHTML;
+ btn.dataset.originalText = originalText;
+ btn.innerHTML = ' 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 = ' Gespielt';
+ currentSoundButton = btn;
+
+ // Optional: Automatisch nach 3–5 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 || ' 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;
\ No newline at end of file
diff --git a/static/js/ui-status.js b/static/js/ui-status.js
new file mode 100644
index 0000000..bf449bc
--- /dev/null
+++ b/static/js/ui-status.js
@@ -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 = ' Verbunden' + (message ? ` – ${message}` : '');
+ if (reconnect) reconnect.style.display = 'none';
+ } else {
+ badge.className = 'badge bg-danger px-3 py-2 fs-6';
+ badge.innerHTML = ' 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;
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
index 328ec23..617cccb 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -86,9 +86,18 @@
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous">
-
-
+
+
+
+
+
+
+
- {% block scripts %}{% endblock %}
+
+
+
+
+{% block scripts %}{% endblock %}