update for app.js
This commit is contained in:
parent
b8b2aec67d
commit
d081477d63
21
app.py
21
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}")
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "Standard-Sounds (global)",
|
||||
"global_sounds": [
|
||||
{
|
||||
"id": "dampflok-fahrt",
|
||||
444
static/js/app.js
444
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 = '<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
16
static/js/config.js
Normal 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;
|
||||
33
static/js/connection-check.js
Normal file
33
static/js/connection-check.js
Normal 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
89
static/js/ui-channels.js
Normal 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
72
static/js/ui-connect.js
Normal 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
135
static/js/ui-soundboard.js
Normal 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 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 || '<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
30
static/js/ui-status.js
Normal 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;
|
||||
@ -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>
|
||||
@ -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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user