<?php
require __DIR__ . '/../config.php';
require FUNCTIONS_ROOT . '/functions_db.php';
require FUNCTIONS_ROOT . '/functions_wecker.php';
require FUNCTIONS_ROOT . '/functions_common.php';

$wecker = function_wecker_hole_alle();
$wochentage_map = function_wecker_hole_wochentage_alle();
$platzhalter_system = function_tts_hole_aktive_platzhalter();
$platzhalter_custom = function_tts_custom_hole_alle();
$tts_models = function_tts_models_laden();
$wochentage_labels = function_wochentage_labels();
$wecker_fuer_js = function_wecker_fuer_frontend($wecker, $wochentage_map);
$zeitzonen = timezone_identifiers_list();
$sleep_mode_sekunden = (int)(function_einstellung_hole('sleep_mode_sekunden') ?? 0);
$accent_color_server = trim((string)(function_einstellung_hole('accent_color') ?? ''));
if ($accent_color_server !== '' && !preg_match('/^#[0-9a-fA-F]{3}$|^#[0-9a-fA-F]{6}$/', $accent_color_server)) {
    $accent_color_server = '';
}
sort($zeitzonen);
?>
<!DOCTYPE html>
<html lang="de" id="htmlRoot">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#ffffff" id="metaThemeColor">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Wecker">
<title>PiClock</title>
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/icon.php?name=icon.svg">
<script>var __INITIAL_ACCENT = <?php echo json_encode($accent_color_server); ?>;</script>
<script>
(function(){
    var t = localStorage.getItem('wecker-theme');
    if (t !== 'light' && t !== 'dark') {
        t = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    }
    document.documentElement.setAttribute('data-theme', t);
    var m = document.getElementById('metaThemeColor');
    if (m) m.content = t === 'dark' ? '#0d1117' : '#ffffff';
    var accent = localStorage.getItem('wecker-accent');
    if (!accent && typeof __INITIAL_ACCENT === 'string' && __INITIAL_ACCENT) accent = __INITIAL_ACCENT;
    if (accent) document.documentElement.style.setProperty('--accent', accent);
})();
</script>
<link rel="stylesheet" href="style.css">
</head>
<body>

<div id="loadingScreen" aria-live="polite" aria-busy="true">
    <div class="spinner" aria-hidden="true"></div>
    <span class="text">Laden…</span>
</div>

<div id="panelAlarm" class="panel active">
<header class="panel-header">
    <div class="header-top">
        <div>
            <h1 class="header-title" id="headerTitle">Alarm <span id="weckerLiveIndicator" class="live-dot" title="Live-Sync aktiv"></span></h1>
            <p class="header-subtitle" id="headerSubtitle">–</p>
        </div>
        <div class="header-actions">
            <button type="button" class="header-btn" id="btnNeuWecker" aria-label="Neuer Alarm">+</button>
            <button type="button" class="header-btn" id="themeToggle" aria-label="Dark/Light Mode" title="Dark/Light Mode"></button>
            <button type="button" class="header-btn btn-open-settings" aria-label="Einstellungen"><img src="/icon.php?name=settings.svg" alt="" class="header-btn-icon theme-icon" width="22" height="22" aria-hidden="true"></button>
        </div>
    </div>
    <div class="header-update-bar" id="updateBar" aria-live="polite">
        <button type="button" class="header-update-btn" id="updateToastToSettings">&#8593; Update verfügbar</button>
        <button type="button" class="header-update-close" id="updateToastClose" aria-label="Schließen">&times;</button>
    </div>
</header>
<main class="main" id="weckerListe">
<?php
foreach ($wecker as $w):
    $aktiv = (bool)($w['aktiv'] ?? 0);
    $wochentage = ($w['wiederholung'] ?? '') === 'woechentlich' ? ($wochentage_map[(int)$w['id']] ?? []) : [];
    if (($w['wiederholung'] ?? '') === 'woechentlich' && !empty($wochentage)) {
        $meta = implode(' ', array_map(function ($t) use ($wochentage_labels) { return $wochentage_labels[$t][0] ?? ''; }, $wochentage));
    } elseif (($w['wiederholung'] ?? '') === 'einmal' && !empty($w['weckdatum'])) {
        $d = DateTime::createFromFormat('Y-m-d', $w['weckdatum']);
        $monate = [1=>'Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
        $tage_kurz = ['Sun'=>'So','Mon'=>'Mo','Tue'=>'Di','Wed'=>'Mi','Thu'=>'Do','Fri'=>'Fr','Sat'=>'Sa'];
        if ($d) {
            $meta = ($tage_kurz[$d->format('D')] ?? $d->format('D')) . ', ' . $d->format('j') . '. ' . ($monate[(int)$d->format('n')] ?? $d->format('F'));
        } else {
            $meta = '';
        }
    } else {
        $meta = 'Täglich';
    }
    $label = $w['sprachtext'] ? (mb_strlen($w['sprachtext']) > 25 ? mb_substr($w['sprachtext'], 0, 22) . '…' : $w['sprachtext']) : '';
?>
    <div class="wecker <?= $aktiv ? '' : 'inaktiv' ?>" data-id="<?= (int)$w['id'] ?>" onclick="editWecker(<?= (int)$w['id'] ?>)">
        <div class="wecker-left">
            <?php if ($label !== ''): ?><span class="wecker-label"><?= htmlspecialchars($label) ?></span><?php endif; ?>
            <div class="weckzeit"><?= htmlspecialchars(substr($w['weckzeit'], 0, 5)) ?></div>
            <div class="wecker-meta"><?= htmlspecialchars($meta) ?></div>
        </div>
        <label class="toggle" onclick="event.stopPropagation()">
            <input type="checkbox" <?= $aktiv ? 'checked' : '' ?> onchange="toggleWecker(<?= (int)$w['id'] ?>, this.checked)">
            <span></span>
        </label>
    </div>
<?php endforeach; ?>
</main>
</div>

<div id="panelTimer" class="panel">
    <header class="panel-header">
        <div class="header-top">
            <div>
                <h1 class="header-title">Timer</h1>
                <p class="header-subtitle header-subtitle-empty">–</p>
            </div>
            <div class="header-actions">
                <button type="button" class="header-btn" id="themeToggle4" aria-label="Dark/Light Mode"></button>
                <button type="button" class="header-btn btn-open-settings" aria-label="Einstellungen"><img src="/icon.php?name=settings.svg" alt="" class="header-btn-icon theme-icon" width="22" height="22" aria-hidden="true"></button>
            </div>
        </div>
    </header>
    <div class="panel-inner timer-inner">
        <div id="timerPresets" class="timer-presets" aria-label="Timer-Voreinstellungen"></div>
        <div class="timer-set">
            <input type="number" id="timerMin" min="0" max="99" value="5" placeholder="Min">
            <span>:</span>
            <input type="number" id="timerSek" min="0" max="59" value="0" placeholder="Sek">
        </div>
        <div id="timerAnzeige" class="timer-anzeige" style="display:none;">05:00</div>
        <div class="timer-buttons">
            <button type="button" class="header-btn" id="timerStart">Start</button>
            <button type="button" class="header-btn" id="timerStop" style="display:none;">Stop</button>
        </div>
    </div>
</div>

<div id="timerPresetEditOverlay" class="overlay" role="dialog" aria-modal="true" aria-labelledby="timerPresetEditTitle">
    <div id="timerPresetEditBox" class="overlay-box" onclick="event.stopPropagation()">
        <h3 id="timerPresetEditTitle">Voreinstellung bearbeiten</h3>
        <label class="timer-preset-edit-label" for="timerPresetEditMin">Minuten</label>
        <input type="number" id="timerPresetEditMin" class="timer-preset-edit-input" min="1" max="99" value="15" placeholder="Min">
        <div class="timer-preset-edit-actions">
            <button type="button" class="header-btn" id="timerPresetEditCancel">Abbrechen</button>
            <button type="button" class="header-btn header-btn-primary" id="timerPresetEditSave">Speichern</button>
        </div>
    </div>
</div>

<div id="panelWetter" class="panel">
    <header class="panel-header">
        <div class="header-top">
            <div>
                <h1 class="header-title">Wetter</h1>
                <p class="header-subtitle" id="wetterSubtitle">Ort in Einstellungen festlegen</p>
            </div>
            <div class="header-actions">
                <button type="button" class="header-btn" id="wetterAktualisieren" aria-label="Aktualisieren">Aktualisieren</button>
                <button type="button" class="header-btn" id="themeToggle5" aria-label="Dark/Light Mode"></button>
                <button type="button" class="header-btn btn-open-settings" aria-label="Einstellungen"><img src="/icon.php?name=settings.svg" alt="" class="header-btn-icon theme-icon" width="22" height="22" aria-hidden="true"></button>
            </div>
        </div>
    </header>
    <div class="panel-inner wetter-dashboard">
        <div class="sleep-panels">
            <div class="sleep-left" id="wetterAktuell"></div>
            <div class="sleep-center" id="wetterForecast"></div>
            <div class="sleep-right" id="wetterDetails"></div>
        </div>
    </div>
</div>

<div id="panelMusik" class="panel">
    <header class="panel-header">
        <div class="header-top">
            <div>
                <h1 class="header-title">Meine Musik</h1>
                <p class="header-subtitle" id="musikSubtitle">Bereit</p>
            </div>
            <div class="header-actions">
                <button type="button" class="header-btn" id="themeToggleMusik" aria-label="Dark/Light Mode"></button>
                <button type="button" class="header-btn btn-open-settings" aria-label="Einstellungen"><img src="/icon.php?name=settings.svg" alt="" class="header-btn-icon theme-icon" width="22" height="22" aria-hidden="true"></button>
            </div>
        </div>
    </header>

    <div class="musik-layout">

        <!-- Linke Seite: Player -->
        <div class="musik-player">
            <div class="player-cover">
                <img id="playerCoverImg" src="" alt="" style="display:none"
                     onerror="this.style.display='none';document.getElementById('playerCoverFallback').style.display='flex'">
                <div id="playerCoverFallback" class="player-cover-fallback">
                    <svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                        <path d="M9 18V5l12-2v13"/>
                        <circle cx="6" cy="18" r="3"/>
                        <circle cx="18" cy="16" r="3"/>
                    </svg>
                </div>
            </div>

            <div class="player-info">
                <div class="player-title" id="playerTitle">Kein Titel</div>
                <div class="player-playlist-name" id="playerPlaylistName">&#x2014;</div>
            </div>

            <div class="player-progress">
                <span class="player-time" id="playerTimeCurrent">0:00</span>
                <input type="range" class="player-seekbar" id="playerSeekbar" min="0" max="100" value="0" step="0.1">
                <span class="player-time player-time-end" id="playerTimeDuration">0:00</span>
            </div>

            <div class="player-controls">
                <button class="player-btn" id="playerPrev" title="Vorheriger Titel" aria-label="Vorheriger Titel">
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
                        <polygon points="19,20 9,12 19,4"/><rect x="5" y="4" width="3" height="16" rx="1"/>
                    </svg>
                </button>
                <button class="player-btn-play" id="playerPlayPause" title="Play / Pause" aria-label="Play / Pause">
                    <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" id="playerPlayIcon" aria-hidden="true">
                        <polygon points="5,3 19,12 5,21"/>
                    </svg>
                </button>
                <button class="player-btn" id="playerNext" title="N&#228;chster Titel" aria-label="N&#228;chster Titel">
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
                        <polygon points="5,4 15,12 5,20"/><rect x="16" y="4" width="3" height="16" rx="1"/>
                    </svg>
                </button>
            </div>

            <div class="player-volume">
                <button class="player-volume-btn" id="playerVolumeBtn" aria-label="Stummschalten">
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="playerVolumeIcon" aria-hidden="true">
                        <polygon points="11,5 6,9 2,9 2,15 6,15 11,19"/>
                        <path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
                        <path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
                    </svg>
                </button>
                <input type="range" class="player-volume-slider" id="playerVolumeSlider" min="0" max="100" value="80" aria-label="Lautst&#228;rke">
            </div>

            <!-- Equalizer -->
            <div class="player-eq-wrap">
                <div class="player-eq-header">
                    <button class="player-eq-toggle" id="eqToggleBtn" aria-expanded="false" aria-controls="playerEq">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
                            <line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/>
                            <line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/>
                            <line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/>
                            <line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/>
                            <line x1="17" y1="16" x2="23" y2="16"/>
                        </svg>
                        <span>Equalizer</span>
                        <svg class="eq-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
                            <polyline points="6 9 12 15 18 9"/>
                        </svg>
                    </button>
                    <button class="player-btn player-eq-reset" id="eqResetBtn" title="Auf 0 zur&#252;cksetzen" aria-label="EQ zur&#252;cksetzen" style="display:none; padding:4px 8px; font-size:0.72rem; border-radius:6px;">
                        Reset
                    </button>
                </div>
                <div class="player-eq" id="playerEq" aria-hidden="true">
                    <div class="eq-bands">
                        <div class="eq-band">
                            <span class="eq-db" id="eqDb0">0</span>
                            <input type="range" class="eq-slider" id="eqSlider0" orient="vertical" min="-12" max="12" value="0" step="1" aria-label="60 Hz">
                            <span class="eq-label">60</span>
                        </div>
                        <div class="eq-band">
                            <span class="eq-db" id="eqDb1">0</span>
                            <input type="range" class="eq-slider" id="eqSlider1" orient="vertical" min="-12" max="12" value="0" step="1" aria-label="250 Hz">
                            <span class="eq-label">250</span>
                        </div>
                        <div class="eq-band">
                            <span class="eq-db" id="eqDb2">0</span>
                            <input type="range" class="eq-slider" id="eqSlider2" orient="vertical" min="-12" max="12" value="0" step="1" aria-label="1 kHz">
                            <span class="eq-label">1k</span>
                        </div>
                        <div class="eq-band">
                            <span class="eq-db" id="eqDb3">0</span>
                            <input type="range" class="eq-slider" id="eqSlider3" orient="vertical" min="-12" max="12" value="0" step="1" aria-label="4 kHz">
                            <span class="eq-label">4k</span>
                        </div>
                        <div class="eq-band">
                            <span class="eq-db" id="eqDb4">0</span>
                            <input type="range" class="eq-slider" id="eqSlider4" orient="vertical" min="-12" max="12" value="0" step="1" aria-label="16 kHz">
                            <span class="eq-label">16k</span>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- Rechte Seite: Bibliothek -->
        <div class="musik-library">
            <div class="musik-subtabs" id="musikSubtabs">
                <button type="button" class="musik-subtab-btn active" data-musik-tab="lokal">Musik</button>
                <button type="button" class="musik-subtab-btn" data-musik-tab="radio">Radio Browser (DE)</button>
            </div>
            <div class="musik-tab-pane active" id="musikTabLokal">
                <div class="musik-filter-bar" id="musikFilterBar">
                    <button class="musik-filter-btn active" data-playlist-filter="all">Alle</button>
                </div>
                <ul class="musik-tracklist" id="musikTracklist"></ul>
                <p id="musikListeLeer" class="platzhalter-hint" style="display:none; padding:16px 14px;">
                    Noch keine Musik vorhanden. Klicke &#x201E;Hochladen&#x201C; zum Hinzuf&#252;gen (MP3, WAV, OGG, M4A).
                </p>
                <div class="musik-library-footer">
                    <button type="button" class="header-btn musik-footer-btn musik-footer-btn-primary" id="musikUploadBtn">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                            <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
                            <polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
                        </svg>
                        Hochladen
                    </button>
                    <button type="button" class="header-btn musik-footer-btn" id="musikVerwaltenBtn">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                            <line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/>
                            <line x1="8" y1="18" x2="21" y2="18"/>
                            <line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/>
                            <line x1="3" y1="18" x2="3.01" y2="18"/>
                        </svg>
                        Playlists
                    </button>
                    <span id="musikUploadStatus" class="musik-upload-status-bar"></span>
                </div>
            </div>
            <div class="musik-tab-pane" id="musikTabRadio">
                <div class="musik-radio-search-wrap">
                    <input type="text" id="musikRadioSearch" class="musik-radio-search-input" placeholder="Deutsche Radiosender suchen …" autocomplete="off">
                </div>
                <ul class="musik-tracklist" id="musikRadioTracklist"></ul>
                <p id="musikRadioLeer" class="platzhalter-hint" style="display:none; padding:16px 14px;">
                    Keine Radiosender gefunden.
                </p>
            </div>
        </div>
    </div>

    <!-- Hidden: Datei-Input + Playlist-Checkboxen -->
    <input type="file" id="musikFileInput" accept=".mp3,.wav,.ogg,.m4a,.aac" multiple style="display:none">
    <div id="musikPlaylistCheckboxes" style="display:none"></div>

    <!-- Upload-Overlay: Playlist auswaehlen -->
    <div id="musikUploadOverlay" class="musik-edit-overlay" aria-hidden="true">
        <div class="musik-edit-box">
            <h3 class="musik-edit-title">Dateien hochladen</h3>
            <label class="musik-edit-label">Playlist zuordnen (optional)</label>
            <div id="musikUploadPlaylistWrap" class="musik-edit-playlists" style="margin-bottom:8px;"></div>
            <div class="musik-edit-actions">
                <button type="button" class="header-btn" id="musikUploadCancel">Abbrechen</button>
                <button type="button" class="header-btn header-btn-primary" id="musikUploadStart">Dateien w&#228;hlen &#x2026;</button>
            </div>
        </div>
    </div>

    <!-- Playlist-Verwaltungs-Overlay -->
    <div id="musikManageOverlay" class="musik-edit-overlay" aria-hidden="true">
        <div class="musik-edit-box" style="max-width:420px;">
            <div class="musik-manage-header">
                <h3>Playlists</h3>
                <button type="button" class="header-btn" id="musikManageClose" style="min-width:36px;min-height:36px;">&#x2715;</button>
            </div>
            <div class="musik-alben-create">
                <input type="text" id="musikAlbumNeu" class="musik-album-input" placeholder="Neue Playlist &#x2026;" maxlength="255">
                <button type="button" class="header-btn" id="musikAlbumErstellen">Erstellen</button>
            </div>
            <ul id="musikAlbenListe" class="musik-alben-liste" style="margin-top:12px;"></ul>
        </div>
    </div>

    <!-- Playlist bearbeiten -->
    <div id="albumEditOverlay" class="musik-edit-overlay" aria-hidden="true">
        <div class="musik-edit-box">
            <h3 class="musik-edit-title">Playlist bearbeiten</h3>
            <input type="hidden" id="albumEditId" value="">
            <label class="musik-edit-label">Name</label>
            <input type="text" id="albumEditName" class="musik-edit-input" maxlength="255" placeholder="Playlistname">
            <label class="musik-edit-label">Icon</label>
            <div class="album-edit-icon-row">
                <div class="musik-cover-wrap album-edit-cover"><img id="albumEditPreview" src="" alt="" width="48" height="48" onerror="this.style.display='none'"></div>
                <div class="album-edit-icon-actions">
                    <input type="file" id="albumEditIconInput" accept=".svg,.png,.jpg,.jpeg,.gif,.webp,image/*" style="display:none">
                    <button type="button" class="header-btn" id="albumEditIconBtn">Icon hochladen</button>
                    <button type="button" class="header-btn" id="albumEditIconRemove" style="display:none">Icon entfernen</button>
                </div>
            </div>
            <p class="platzhalter-hint">Kein Icon: zuf&#228;lliges Song-Cover aus der Playlist.</p>
            <div class="musik-edit-actions">
                <button type="button" class="header-btn" id="albumEditCancel">Abbrechen</button>
                <button type="button" class="header-btn header-btn-primary" id="albumEditSave">Speichern</button>
            </div>
        </div>
    </div>

    <!-- Song bearbeiten -->
    <div id="musikEditOverlay" class="musik-edit-overlay" aria-hidden="true">
        <div class="musik-edit-box">
            <h3 class="musik-edit-title">Titel &amp; Playlists bearbeiten</h3>
            <input type="hidden" id="musikEditId" value="">
            <label class="musik-edit-label">Titel</label>
            <input type="text" id="musikEditTitel" class="musik-edit-input" maxlength="255" placeholder="Titel">
            <label class="musik-edit-label">Playlists (Mehrfachauswahl)</label>
            <div id="musikEditPlaylists" class="musik-edit-playlists"></div>
            <div class="musik-edit-actions">
                <button type="button" class="header-btn" id="musikEditCancel">Abbrechen</button>
                <button type="button" class="header-btn header-btn-primary" id="musikEditSave">Speichern</button>
            </div>
        </div>
    </div>
</div>


<!-- Alarm-Klingel-Overlay: erscheint wenn ein Wecker klingelt -->
<div id="alarmRingingOverlay" class="alarm-ringing-overlay" aria-hidden="true" role="alertdialog" aria-label="Wecker klingelt">

    <!-- Linke Seite: Cover-Bild oder Fallback-Icon -->
    <div class="alarm-ringing-left">
        <div class="alarm-cover-wrap">
            <img id="alarmCoverImg" src="" alt="Album-Cover" class="alarm-cover-img"
                 onerror="this.style.display='none'; document.getElementById('alarmCoverFallback').style.display='flex'">
            <div id="alarmCoverFallback" class="alarm-cover-fallback">
                <svg width="100" height="100" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M6.5 3.5A1 1 0 0 0 5 4.914L6.086 6A8 8 0 0 0 4 12a8 8 0 1 0 16 0 8 8 0 0 0-2.086-6L19 4.914A1 1 0 0 0 17.5 3.5L16.232 4.768A9.956 9.956 0 0 0 12 4c-1.457 0-2.84.312-4.086.87z" fill="#58a6ff" opacity=".3"/>
                    <circle cx="12" cy="12" r="8" stroke="#58a6ff" stroke-width="1.5" fill="none"/>
                    <path d="M12 7v5.5l3.5 2" stroke="#58a6ff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
                    <path d="M5 3L3 5M19 3l2 2" stroke="#58a6ff" stroke-width="1.5" stroke-linecap="round"/>
                </svg>
            </div>
        </div>
    </div>

    <!-- Rechte Seite: Zeit, Buttons -->
    <div class="alarm-ringing-right">
        <div class="alarm-ringing-label">Wecker klingelt</div>
        <div class="alarm-time-display" id="alarmRingingTime">00:00</div>
        <div class="alarm-ringing-title" id="alarmRingingTitle"></div>
        <button type="button" class="alarm-play-audio-btn" id="alarmPlayAudioBtn" style="display:none" aria-label="Ton starten">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                <polygon points="11,5 6,9 2,9 2,15 6,15 11,19"/>
                <path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
                <path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
            </svg>
            Ton starten
        </button>
        <div class="alarm-ringing-actions">
            <button type="button" class="alarm-snooze-btn" id="alarmSnoozeBtn">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M12 6v6l3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                    <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
                </svg>
                Snooze (5 Min)
            </button>
            <button type="button" class="alarm-stop-btn" id="alarmStopBtn">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <rect x="6" y="6" width="12" height="12" rx="2" fill="white"/>
                </svg>
                Stop
            </button>
        </div>
    </div>

</div>

<!-- Sleep-Mode Vollbild: Uhr + 3 Spalten (Aktuell | 8-Tage | Details) + Wecker-Leiste -->
<div id="sleepModeOverlay" class="sleep-mode-overlay" aria-hidden="true">
    <div id="sleepModeTime" class="sleep-mode-time" aria-live="polite">
        <span id="sleepModeTimeClock" class="sleep-mode-time-clock">00:00</span>
        <span id="sleepModeTimeDate" class="sleep-mode-time-date"></span>
    </div>
    <div id="sleepMusicPlayer" class="sleep-music-player" aria-label="Musiksteuerung im Sleep-Mode" style="display:none;">
        <div class="sleep-music-cover">
            <img id="sleepPlayerCoverImg" src="" alt="" style="display:none">
            <div id="sleepPlayerCoverFallback" class="sleep-music-cover-fallback">
                <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
                    <path d="M9 18V5l12-2v13"></path>
                    <circle cx="6" cy="18" r="3"></circle>
                    <circle cx="18" cy="16" r="3"></circle>
                </svg>
            </div>
        </div>
        <div class="sleep-music-info">
            <div id="sleepPlayerTitle" class="sleep-music-title">Kein Lied ausgewählt</div>
            <div id="sleepPlayerMeta" class="sleep-music-meta">Bereit</div>
        </div>
        <div class="sleep-music-controls">
            <button type="button" class="sleep-music-btn sleep-music-btn-play" id="sleepPlayerPlayPause" aria-label="Play / Pause">
                <svg id="sleepPlayerPlayIcon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
                    <polygon points="5,3 19,12 5,21"></polygon>
                </svg>
            </button>
            <button type="button" class="sleep-music-btn" id="sleepPlayerVolumeBtn" aria-label="Stummschalten">
                <svg id="sleepPlayerVolumeIcon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                    <polygon points="11,5 6,9 2,9 2,15 6,15 11,19"></polygon>
                    <path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
                    <path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
                </svg>
            </button>
            <input type="range" id="sleepPlayerVolumeSlider" class="sleep-music-volume" min="0" max="100" value="80" aria-label="Lautstärke">
        </div>
    </div>
    <div class="sleep-panels">
        <div class="sleep-left" id="sleepWetterAktuell"></div>
        <div class="sleep-center" id="sleepWetterForecast"></div>
        <div class="sleep-right" id="sleepWetterDetails"></div>
    </div>
    <div class="sleep-wecker-bar">
        <div class="sleep-title">Nächste Wecker</div>
        <div id="sleepWeckerListe"></div>
    </div>
</div>

<nav class="bottom-nav" aria-label="Hauptnavigation">
    <button type="button" data-panel="panelAlarm" class="active">Alarm</button>
    <button type="button" data-panel="panelTimer">Timer</button>
    <button type="button" data-panel="panelWetter">Wetter</button>
    <button type="button" data-panel="panelMusik">Musik</button>
</nav>


<!-- Edit Overlay -->
<div id="editOverlay" class="overlay" onclick="closeEdit()">
    <div id="editBox" class="overlay-box" onclick="event.stopPropagation()">
        <div id="editOverlayLoading" class="overlay-loading" style="display:none;">
            <div class="spinner-small"></div>
            <span class="text">Wecker wird geladen…</span>
        </div>
        <div id="editBoxContent">
        <h2 id="editTitle">Wecker bearbeiten</h2>
        <form id="editForm" onsubmit="return saveWecker(event)">
            <input type="hidden" name="id" id="editId" value="0">
            <div class="form-row">
                <label>Weckzeit</label>
                <input type="time" name="weckzeit" id="editWeckzeit" required>
            </div>
            <div class="form-row">
                <label>Wiederholung</label>
                <select name="wiederholung" id="editWiederholung" onchange="toggleWochentageVisibility()">
                    <option value="einmal">Einmal</option>
                    <option value="taeglich">Täglich</option>
                    <option value="woechentlich">Wöchentlich</option>
                </select>
            </div>
            <div class="form-row" id="rowWeckdatum">
                <label>Datum (nur bei „Einmal“)</label>
                <input type="date" name="weckdatum" id="editWeckdatum">
            </div>
            <div class="form-row" id="rowWochentage" style="display:none">
                <label>Wochentage</label>
                <div class="wochentage-row" id="wochentageRow">
                    <?php foreach ($wochentage_labels as $num => $label): ?>
                    <label id="tagLabel<?= $num ?>">
                        <input type="checkbox" name="wochentage[]" value="<?= $num ?>" id="tagCb<?= $num ?>">
                        <span><?= $label ?></span>
                    </label>
                    <?php endforeach; ?>
                </div>
            </div>
            <div class="form-row">
                <label>Sprachtext (TTS)</label>
                <div class="sprachtext-toggle-row">
                    <button type="button" class="sprachtext-toggle-btn active" id="editSprachtextTogglePlaceholder" aria-pressed="true">Platzhalter</button>
                    <button type="button" class="sprachtext-toggle-btn" id="editSprachtextTogglePreview" aria-pressed="false">Vorschau</button>
                </div>
                <div id="editSprachtextWrap" class="sprachtext-edit-wrap">
                    <textarea name="sprachtext" id="editSprachtext" required></textarea>
                </div>
                <div id="editSprachtextPreview" class="sprachtext-preview-wrap" style="display:none;" aria-hidden="true"></div>
                <div class="form-row">
                    <button type="button" class="header-btn" id="editTtsGeneratePlay" aria-label="TTS erzeugen und abspielen">
                        TTS erzeugen &amp; abspielen
                    </button>
                    <span id="editTtsGenerateStatus" class="platzhalter-hint" style="margin-left:8px;" aria-live="polite"></span>
                </div>
                <p class="platzhalter-hint">Platzhalter per Drag &amp; Drop in den Text ziehen oder antippen (Touch) zum Einfügen an der Cursorposition:</p>
                <div class="platzhalter-chips" id="editPlatzhalterChips">
                    <?php
                    $alle_platzhalter = [];
                    foreach ($platzhalter_system as $p) {
                        $v = $p['platzhalter'] ?? '';
                        if ($v !== '') $alle_platzhalter[] = (strpos($v, '{{') !== false ? $v : '{{' . $v . '}}');
                    }
                    foreach ($platzhalter_custom as $p) {
                        $v = $p['platzhalter'] ?? '';
                        if ($v !== '') $alle_platzhalter[] = (strpos($v, '{{') !== false ? $v : '{{' . $v . '}}');
                    }
                    $alle_platzhalter = array_unique($alle_platzhalter);
                    foreach ($alle_platzhalter as $ph):
                    ?><span class="platzhalter-chip" draggable="true" data-placeholder="<?= htmlspecialchars($ph) ?>" title="Ziehen oder antippen zum Einfügen"><?= htmlspecialchars($ph) ?></span><?php endforeach; ?>
                </div>
            </div>
            <div class="form-row">
                <label>TTS-Lautstärke (0–100)</label>
                <input type="range" name="lautstaerke_tts" id="editLautstaerkeTts" min="0" max="100" value="100">
                <span id="editLautstaerkeTtsVal">100</span> %
            </div>
            <div class="form-row">
                <label>Musik-/Audio-Lautstärke (0–100)</label>
                <input type="range" name="lautstaerke_musik" id="editLautstaerkeMusik" min="0" max="100" value="80">
                <span id="editLautstaerkeMusikVal">80</span> %
            </div>
            <div class="form-row">
                <label>Während TTS: Musik dämpfen auf … %</label>
                <input type="range" name="musik_ducking_prozent" id="editMusikDucking" min="0" max="100" value="20">
                <span id="editMusikDuckingVal">20</span> %
                <p class="platzhalter-hint">Beim TTS-Spruch wird die Musik auf diesen Wert reduziert, damit die Ansage gut hörbar ist.</p>
            </div>
            <div class="form-row audio-section">
                <label>Musik beim Wecken</label>
                <p class="platzhalter-hint">Optional: Playlist, einzelnes Lied oder Radiosender (z. B. Antenne Bayern, ROCK ANTENNE) – gleichzeitig mit TTS oder danach.</p>
                <div class="audio-groups">
                    <div class="audio-group">
                        <span class="audio-group-title">Gleichzeitig mit TTS</span>
                        <ul id="audioListGleichzeitig" class="audio-list"></ul>
                        <div class="audio-search-wrap">
                            <input type="text" id="audioSearchGleichzeitig" class="audio-search-input" placeholder="Playlist, Lied oder Radiosender suchen…" autocomplete="off" data-position="gleichzeitig">
                            <ul id="audioSearchResultsGleichzeitig" class="audio-search-results" aria-hidden="true"></ul>
                        </div>
                    </div>
                    <div class="audio-group">
                        <span class="audio-group-title">Danach</span>
                        <ul id="audioListDanach" class="audio-list"></ul>
                        <div class="audio-search-wrap">
                            <input type="text" id="audioSearchDanach" class="audio-search-input" placeholder="Playlist, Lied oder Radiosender suchen…" autocomplete="off" data-position="danach">
                            <ul id="audioSearchResultsDanach" class="audio-search-results" aria-hidden="true"></ul>
                        </div>
                    </div>
                </div>
                <input type="hidden" name="audio" id="editAudio" value="[]">
            </div>
            <div class="form-actions">
                <button type="submit" class="header-btn">Speichern</button>
                <button type="button" class="header-btn" onclick="closeEdit()">Abbrechen</button>
                <button type="button" id="btnDeleteWecker" class="header-btn danger" onclick="deleteWecker()" style="display:none">Löschen</button>
            </div>
        </form>
        </div>
    </div>
</div>

<!-- Spiele-Menü (Easter Egg: 5x auf Aktuelle Version) -->
<div id="spieleMenuOverlay" class="overlay" style="display:none;" role="dialog" aria-modal="true" aria-labelledby="spieleMenuTitle" onclick="closeSpieleMenu(event)">
    <div class="overlay-box spiele-menu-box" onclick="event.stopPropagation()">
        <div class="spiele-menu-header">
            <h2 id="spieleMenuTitle">Spiele</h2>
            <button type="button" class="spiele-menu-close" onclick="closeSpieleMenu()" aria-label="Schließen">&times;</button>
        </div>
        <p class="spiele-menu-subtitle">Wähle ein Spiel.</p>
        <div class="spiele-menu-buttons">
            <a href="games/pferderennen.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">🐎</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Pferderennen</span>
                    <span class="spiele-btn-desc">Zufallsrennen mit vier Pferden</span>
                </span>
            </a>
            <a href="games/tetris.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">🧱</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Tetris</span>
                    <span class="spiele-btn-desc">Steine setzen und Reihen abräumen</span>
                </span>
            </a>
            <a href="games/snake.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">🐍</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Snake</span>
                    <span class="spiele-btn-desc">Wachsen ohne gegen Wände zu stoßen</span>
                </span>
            </a>
            <a href="games/dreigewinnt.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">❌⭕</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">3 Gewinnt</span>
                    <span class="spiele-btn-desc">Klassisches Duell X gegen O</span>
                </span>
            </a>
            <a href="games/schach.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">♟️</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Schach</span>
                    <span class="spiele-btn-desc">Multiplayer oder gegen Bot spielen</span>
                </span>
            </a>
            <a href="games/solitaer.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">🃏</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Solitär</span>
                    <span class="spiele-btn-desc">Kartenstapel ordnen und Zielstapel füllen</span>
                </span>
            </a>
            <a href="games/blackjack.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">🂡</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Blackjack</span>
                    <span class="spiele-btn-desc">21 erreichen und Dealer schlagen</span>
                </span>
            </a>
            <a href="games/roulette.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">🎯</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Roulette</span>
                    <span class="spiele-btn-desc">Auf Farben, Gerade/Ungerade oder Zahl setzen</span>
                </span>
            </a>
            <a href="games/slotmaschine.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">🎰</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Slotmaschine</span>
                    <span class="spiele-btn-desc">Einsatz drehen und Symbole kombinieren</span>
                </span>
            </a>
            <a href="games/minesweeper.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">⏰</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Minesweeper</span>
                    <span class="spiele-btn-desc">Alarmfallen aufdecken und entschärfen</span>
                </span>
            </a>
            <a href="games/sudoku.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">🧩</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Sudoku</span>
                    <span class="spiele-btn-desc">Zahlenrätsel als Weckerplan</span>
                </span>
            </a>
            <a href="games/memory.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">🧠</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Memory</span>
                    <span class="spiele-btn-desc">Wecker-Symbole als Kartenpaare</span>
                </span>
            </a>
            <a href="games/towerdefense_mini.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">🛡️</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Tower Defense mini</span>
                    <span class="spiele-btn-desc">Schlafzimmer vor Alarmwellen schützen</span>
                </span>
            </a>
            <a href="games/quiz_wecker.html" class="spiele-btn">
                <span class="spiele-btn-icon" aria-hidden="true">🕰️</span>
                <span class="spiele-btn-content">
                    <span class="spiele-btn-title">Quiz-Game</span>
                    <span class="spiele-btn-desc">Fragen rund um Wecker und Aufstehen</span>
                </span>
            </a>
        </div>
        <div class="spiele-menu-footer">
            <button type="button" class="spiele-menu-footer-btn" onclick="closeSpieleMenu()">Schließen</button>
        </div>
    </div>
</div>

<!-- Settings Overlay -->
<div id="settingsOverlay" class="overlay" onclick="closeSettings(event)">
    <div class="overlay-box" onclick="event.stopPropagation()" style="position:relative;">
        <div id="settingsOverlayLoading" class="overlay-loading" style="display:none; position:absolute; inset:0; background:var(--card); border-radius:12px;">
            <div class="spinner-small"></div>
            <span class="text">Einstellungen werden geladen…</span>
        </div>
        <div id="settingsBoxContent">
        <div class="settings-overlay-header">
            <h2>Einstellungen</h2>
            <button type="button" class="settings-overlay-close" onclick="closeSettings()" aria-label="Schließen"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg></button>
        </div>
        <div class="settings-tabs-wrapper">
        <nav class="settings-tabs" role="tablist" aria-label="Einstellungsbereiche">
            <button type="button" role="tab" id="settingsTabBtnAllgemein" aria-selected="true" aria-controls="settingsTabAllgemein" data-tab="settingsTabAllgemein" class="active">Allgemein</button>
            <button type="button" role="tab" id="settingsTabBtnTTS" aria-selected="false" aria-controls="settingsTabTTS" data-tab="settingsTabTTS">TTS</button>
            <button type="button" role="tab" id="settingsTabBtnPlatzhalter" aria-selected="false" aria-controls="settingsTabPlatzhalter" data-tab="settingsTabPlatzhalter">Platzhalter</button>
            <button type="button" role="tab" id="settingsTabBtnInfo" aria-selected="false" aria-controls="settingsTabInfo" data-tab="settingsTabInfo">Info</button>
            <button type="button" role="tab" id="settingsTabBtnDebug" aria-selected="false" aria-controls="settingsTabDebug" data-tab="settingsTabDebug">Debug</button>
            <button type="button" role="tab" id="settingsTabBtnUpdate" aria-selected="false" aria-controls="settingsTabUpdate" data-tab="settingsTabUpdate">Update</button>
            <button type="button" role="tab" id="settingsTabBtnWlan" aria-selected="false" aria-controls="settingsTabWlan" data-tab="settingsTabWlan">WLAN</button>
            <button type="button" role="tab" id="settingsTabBtnStatistik" aria-selected="false" aria-controls="settingsTabStatistik" data-tab="settingsTabStatistik">Statistik</button>
        </nav>
        </div>
        <div class="settings-tab-panels">
        <div id="settingsTabAllgemein" class="settings-tab-panel active" role="tabpanel" aria-labelledby="settingsTabBtnAllgemein">
            <div class="settings-section">
                <h3>Zeitzone</h3>
                <div class="form-row zeitzone-dropdown-wrap">
                    <input type="text" id="settingZeitzone" placeholder="Suchen oder auswählen…" autocomplete="off" role="combobox" aria-expanded="false" aria-autocomplete="list">
                    <ul id="zeitzoneList" class="zeitzone-list" role="listbox" aria-hidden="true"></ul>
                </div>
            </div>
            <div class="settings-section">
                <h3>Name (für TTS)</h3>
                <div class="form-row">
                    <label>Ihr Name</label>
                    <input type="text" id="settingTtsName" placeholder="z.B. Max" maxlength="100" autocomplete="name">
                    <p class="platzhalter-hint">Wird im Wecker-Sprachtext für <code>{{name}}</code> verwendet.</p>
                </div>
            </div>
            <div class="settings-section">
                <h3>Akzentfarbe</h3>
                <div class="form-row form-row-accent">
                    <label>Farbe für Buttons, Links und Hervorhebungen</label>
                    <div class="accent-color-row">
                        <input type="color" id="settingAccentColor" value="#2563eb" title="Akzentfarbe wählen">
                        <input type="text" id="settingAccentColorHex" class="accent-hex-input" placeholder="#2563eb" maxlength="7" pattern="^#[0-9a-fA-F]{3,6}$">
                        <button type="button" class="header-btn" id="settingAccentReset" title="Standard zurücksetzen">Standard</button>
                    </div>
                </div>
            </div>
            <div class="settings-section">
                <h3>Wetter</h3>
                <div class="form-row">
                    <label>Ort oder PLZ (Deutschland)</label>
                    <input type="text" id="settingWetterOrt" placeholder="z.B. 97478 oder Knetzgau" maxlength="100">
                    <p class="platzhalter-hint">Wetter-Anzeige auf der Wetter-Seite. PLZ oder Ortsname (z.B. Knetzgau).</p>
                </div>
            </div>
            <div class="settings-section sleep-mode-section">
                <h3>Sleep-Mode</h3>
                <div class="form-row">
                    <label for="settingSleepModeSekunden">Sleep-Mode starten nach … Sekunden (0 = aus)</label>
                    <input type="number" id="settingSleepModeSekunden" min="0" max="86400" value="0" placeholder="z.B. 30" aria-describedby="sleepModeHint">
                    <p id="sleepModeHint" class="platzhalter-hint">Nach dieser Zeit ohne Berührung wird der Sleep-Mode angezeigt (nur bei Aufruf über localhost bzw. 127.0.0.1 – nicht auf Handy/LAN). Tippen zum Beenden.</p>
                </div>
            </div>
        </div>

        <div id="settingsTabTTS" class="settings-tab-panel" role="tabpanel" aria-labelledby="settingsTabBtnTTS">
            <div class="settings-section">
                <h3>TTS</h3>
                <div class="form-row">
                    <label>Welcher TTS-Anbieter wird verwendet?</label>
                    <select id="settingTtsProvider">
                        <option value="piper">Piper (lokal)</option>
                        <option value="openai">OpenAI (API)</option>
                    </select>
                    <p class="platzhalter-hint">Mit dem Dropdown legen Sie fest, ob TTS über Piper (lokales Skript) oder über die OpenAI-API erzeugt wird.</p>
                </div>
                <div id="ttsPiperOptions" class="tts-provider-options">
                    <div class="form-row">
                        <label>Sprachmodell (Piper)</label>
                        <select id="settingTtsModell">
                            <?php foreach ($tts_models as $m): ?>
                            <option value="<?= htmlspecialchars($m['id']) ?>"><?= htmlspecialchars($m['label']) ?></option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                </div>
                <div id="ttsOpenaiOptions" class="tts-provider-options" style="display:none;">
                    <div class="form-row">
                        <label>OpenAI API-Key</label>
                        <input type="password" id="settingTtsOpenaiApiKey" placeholder="sk-…" autocomplete="off">
                        <p class="platzhalter-hint">Leer lassen, um den gespeicherten Key beizubehalten.</p>
                    </div>
                    <div class="form-row">
                        <label>OpenAI Modell</label>
                        <select id="settingTtsOpenaiModel">
                            <option value="tts-1">tts-1</option>
                            <option value="tts-1-hd">tts-1-hd</option>
                            <option value="gpt-4o-mini-tts">gpt-4o-mini-tts</option>
                            <option value="gpt-4o-mini-tts-2025-12-15">gpt-4o-mini-tts-2025-12-15</option>
                        </select>
                    </div>
                    <div class="form-row">
                        <label>Stimme</label>
                        <select id="settingTtsOpenaiVoice">
                            <option value="alloy">alloy</option>
                            <option value="ash">ash</option>
                            <option value="ballad">ballad</option>
                            <option value="echo">echo</option>
                            <option value="fable">fable</option>
                            <option value="onyx">onyx</option>
                            <option value="nova">nova</option>
                            <option value="shimmer">shimmer</option>
                            <option value="coral">coral</option>
                            <option value="sage">sage</option>
                            <option value="verse">verse</option>
                            <option value="marin">marin</option>
                            <option value="cedar">cedar</option>
                        </select>
                    </div>
                </div>
            </div>
        </div>

        <div id="settingsTabPlatzhalter" class="settings-tab-panel" role="tabpanel" aria-labelledby="settingsTabBtnPlatzhalter">
            <div class="settings-section">
                <h3>Benutzerdefinierte TTS-Platzhalter</h3>
                <p class="platzhalter-hint">Diese Platzhalter können Sie im Wecker-Sprachtext verwenden (z.B. {{name}}).</p>
                <div id="ttsCustomList" class="tts-custom-list"></div>
                <div class="form-row">
                    <label>Platzhalter (z.B. weckername → {{weckername}})</label>
                    <input type="text" id="customPlatzhalter" placeholder="weckername">
                </div>
                <div class="form-row">
                    <label>Ersetzungstext</label>
                    <input type="text" id="customErsetzungstext" placeholder="Guten Morgen">
                </div>
                <div class="form-row">
                    <label>Beschreibung (optional)</label>
                    <input type="text" id="customBeschreibung" placeholder="Begrüßung">
                </div>
                <input type="hidden" id="customId" value="0">
                <button type="button" class="header-btn" id="btnCustomSave">Hinzufügen / Speichern</button>
            </div>
        </div>

                <div id="settingsTabInfo" class="settings-tab-panel" role="tabpanel" aria-labelledby="settingsTabBtnInfo">
            <div class="settings-section">
                <h3>System-Info</h3>
                <div class="form-row">
                    <label>IP-Adresse (Server)</label>
                    <p id="infoIp" class="info-value">–</p>
                </div>
            </div>
            <div class="settings-section">
                <h3>System</h3>
                <p class="platzhalter-hint">Raspberry Pi neu starten oder herunterfahren.</p>
                <div style="display:flex; gap:8px; margin-top:8px; flex-wrap:wrap;">
                    <button type="button" class="header-btn" id="btnReboot">Neu starten</button>
                    <button type="button" class="header-btn danger" id="btnShutdown">Herunterfahren</button>
                </div>
                <p id="systemCmdMsg" class="platzhalter-hint" style="margin-top:8px; display:none;"></p>
            </div>
            <div class="settings-section">
                <h3>Browser</h3>
                <p class="platzhalter-hint">Alle laufenden Firefox-Prozesse beenden.</p>
                <div style="display:flex; gap:8px; margin-top:8px; flex-wrap:wrap;">
                    <button type="button" class="header-btn danger" id="btnKillFirefox">Exit to Desktop</button>
                </div>
                <p id="killFirefoxMsg" class="platzhalter-hint" style="margin-top:8px; display:none;"></p>
            </div>
        </div>

        <div id="settingsTabDebug" class="settings-tab-panel" role="tabpanel" aria-labelledby="settingsTabBtnDebug">
            <div class="settings-section">
                <h3>Cronjobs</h3>
                <div class="form-row">
                    <label>Letzter Cron-Lauf (Master)</label>
                    <p id="infoCronTime" class="info-value">–</p>
                    <p id="infoCronStatus" class="info-cron-status" role="status" aria-live="polite"></p>
                </div>
                <div class="form-row">
                    <label>Einzelne Jobs starten</label>
                    <div id="cronJobList" class="cron-job-list">
                        <p class="platzhalter-hint" id="cronJobListHint">Lade Jobs…</p>
                    </div>
                    <button type="button" class="header-btn" id="btnCronRunOnce" style="margin-top:10px">Alle Jobs starten</button>
                </div>
            </div>
            <div class="settings-section">
                <h3>WebSocket</h3>
                <div class="form-row">
                    <label>Remote-Verbindung</label>
                    <p id="infoWsStatus" class="info-cron-status">Unbekannt</p>
                    <p id="infoWsMeta" class="platzhalter-hint">Noch keine WebSocket-Information vorhanden.</p>
                </div>
            </div>
            <div class="settings-section">
                <h3>Cache</h3>
                <p class="platzhalter-hint">Wetter- und TTS-Cache leeren (z. B. nach Änderungen an Orten oder Sprachtexten).</p>
                <button type="button" class="header-btn" id="btnResetCache" style="margin-top:8px">Cache zurücksetzen</button>
            </div>
            <div class="settings-section">
                <h3>Aufräumen</h3>
                <p class="platzhalter-hint">Alte Backups, Logs und temporäre Dateien analysieren und löschen.</p>
                <button type="button" class="header-btn" id="btnCleanupScan" style="margin-top:8px">Scannen</button>
                <div id="cleanupResult" style="display:none; margin-top:12px;"></div>
            </div>
        </div>
        <div id="settingsTabUpdate" class="settings-tab-panel" role="tabpanel" aria-labelledby="settingsTabBtnUpdate">
            <div class="settings-section">
                <h3>Update</h3>
                <p class="platzhalter-hint">Aktuelle Version vom Update-Server installieren. Vorher wird ein Backup erstellt.</p>
                <div class="form-row">
                    <label>Aktuelle Version</label>
                    <p id="updateCurrentVersion" class="update-version-display">–</p>
                </div>
                <div class="form-row">
                    <label>Version installieren</label>
                    <select id="updateVersionSelect">
                        <option value="">Lade…</option>
                    </select>
                    <p class="platzhalter-hint" id="updateVersionHint"></p>
                </div>
                <div class="form-row">
                    <button type="button" class="header-btn" id="updateInstallBtn" disabled>Installieren</button>
                </div>
                <div id="updateLogWrap" class="update-log-wrap" style="display:none;">
                    <label>Log</label>
                    <pre id="updateLog" class="update-log"></pre>
                    <a href="#" id="updateReloadLink" style="display:none;">Seite neu laden</a>
                </div>
            </div>
        </div>

        <div id="settingsTabWlan" class="settings-tab-panel" role="tabpanel" aria-labelledby="settingsTabBtnWlan">
            <div class="settings-section">
                <h3>WLAN-Status</h3>
                <div id="wlanStatus" class="wlan-status-box">
                    <span class="wlan-status-text">Wird geladen…</span>
                </div>
                <div class="form-row" style="margin-top:10px; display:flex; gap:8px; flex-wrap:wrap;">
                    <button type="button" class="header-btn" id="wlanScanBtn" onclick="wlanScan()">Netzwerke scannen</button>
                    <button type="button" class="header-btn" id="wlanDisconnectBtn" onclick="wlanDisconnect()" style="display:none">Trennen</button>
                </div>
            </div>
            <div class="settings-section" id="wlanNetworkSection" style="display:none">
                <h3>Verfügbare Netzwerke</h3>
                <div id="wlanNetworkList" class="wlan-network-list"></div>
            </div>
            <div class="settings-section" id="wlanConnectSection" style="display:none">
                <h3>Verbinden mit: <span id="wlanConnectSsidLabel"></span></h3>
                <div class="form-row">
                    <label>Passwort</label>
                    <input type="password" id="wlanPassword" placeholder="WLAN-Passwort" autocomplete="new-password">
                </div>
                <div id="wlanConnectMsg" class="wlan-msg" style="display:none"></div>
                <div style="display:flex; gap:8px; flex-wrap:wrap; margin-top:8px;">
                    <button type="button" class="header-btn" id="wlanConnectBtn" onclick="wlanConnect()">Verbinden</button>
                    <button type="button" class="header-btn" onclick="wlanCancelConnect()">Abbrechen</button>
                </div>
            </div>
        </div>

        <div id="settingsTabStatistik" class="settings-tab-panel" role="tabpanel" aria-labelledby="settingsTabBtnStatistik">
            <div style="padding-top:8px;">
                <div class="stats-range-row" id="statsRangeRow">
                    <button class="stats-range-btn" data-range="1h">1 Std</button>
                    <button class="stats-range-btn active" data-range="24h">24 Std</button>
                    <button class="stats-range-btn" data-range="6h">6 Std</button>
                    <button class="stats-range-btn" data-range="7d">7 Tage</button>
                </div>

                <div class="stats-summary-row">
                    <div class="stats-kpi"><span class="kpi-label">Aktuell</span><span class="kpi-value" id="statsCpuCurrent">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Min</span><span class="kpi-value" id="statsCpuMin">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Max</span><span class="kpi-value" id="statsCpuMax">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Durchschnitt</span><span class="kpi-value" id="statsCpuAvg">–</span></div>
                </div>

                <div class="stats-chart-card">
                    <div class="stats-chart-title">CPU-Temperatur (°C)</div>
                    <div class="stats-chart-wrap" id="statsCpuChartWrap">
                        <div class="stats-loading">Lade…</div>
                    </div>
                </div>

                <div class="stats-summary-row">
                    <div class="stats-kpi"><span class="kpi-label">Aktuell</span><span class="kpi-value" id="statsPowerCurrent">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Min</span><span class="kpi-value" id="statsPowerMin">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Max</span><span class="kpi-value" id="statsPowerMax">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Durchschnitt</span><span class="kpi-value" id="statsPowerAvg">–</span></div>
                </div>

                <div class="stats-chart-card">
                    <div class="stats-chart-title">Stromverbrauch (W)</div>
                    <div class="stats-chart-wrap" id="statsPowerChartWrap">
                        <div class="stats-loading">Lade…</div>
                    </div>
                </div>

                <!-- ── Raumklima (SCD41) ── -->
                <div class="stats-section-divider">
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
                        <path d="M12 2a7 7 0 0 1 7 7c0 5-7 13-7 13S5 14 5 9a7 7 0 0 1 7-7z"/>
                        <circle cx="12" cy="9" r="2.5"/>
                    </svg>
                    Raumklima (SCD41)
                </div>

                <div class="stats-summary-row">
                    <div class="stats-kpi"><span class="kpi-label">CO&#x2082; aktuell</span><span class="kpi-value" id="statsCo2Current">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Min</span><span class="kpi-value" id="statsCo2Min">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Max</span><span class="kpi-value" id="statsCo2Max">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Durchschnitt</span><span class="kpi-value" id="statsCo2Avg">–</span></div>
                </div>
                <div class="stats-chart-card">
                    <div class="stats-chart-title">CO&#x2082; (ppm)</div>
                    <div class="stats-chart-wrap" id="statsCo2ChartWrap">
                        <div class="stats-loading">Lade…</div>
                    </div>
                </div>

                <div class="stats-summary-row">
                    <div class="stats-kpi"><span class="kpi-label">Temperatur aktuell</span><span class="kpi-value" id="statsTempCurrent">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Min</span><span class="kpi-value" id="statsTempMin">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Max</span><span class="kpi-value" id="statsTempMax">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Durchschnitt</span><span class="kpi-value" id="statsTempAvg">–</span></div>
                </div>
                <div class="stats-chart-card">
                    <div class="stats-chart-title">Temperatur (°C)</div>
                    <div class="stats-chart-wrap" id="statsTempChartWrap">
                        <div class="stats-loading">Lade…</div>
                    </div>
                </div>

                <div class="stats-summary-row">
                    <div class="stats-kpi"><span class="kpi-label">Luftfeuchte aktuell</span><span class="kpi-value" id="statsHumCurrent">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Min</span><span class="kpi-value" id="statsHumMin">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Max</span><span class="kpi-value" id="statsHumMax">–</span></div>
                    <div class="stats-kpi"><span class="kpi-label">Durchschnitt</span><span class="kpi-value" id="statsHumAvg">–</span></div>
                </div>
                <div class="stats-chart-card">
                    <div class="stats-chart-title">Luftfeuchtigkeit (%)</div>
                    <div class="stats-chart-wrap" id="statsHumChartWrap">
                        <div class="stats-loading">Lade…</div>
                    </div>
                </div>

                <p class="stats-hint">System-Daten minütlich, Raumklima alle 30 Sek. – je 7 Tage gespeichert.</p>
            </div>
        </div>
        </div><!-- /.settings-tab-panels -->
        <div class="settings-overlay-footer">
            <span id="settingsMsg" class="settings-msg"></span>
            <button type="button" class="btn-primary" onclick="saveSettings()">Speichern</button>
            <button type="button" class="btn-secondary" onclick="closeSettings()">Schließen</button>
        </div>
        </div>
    </div>
</div>

<!-- Bildschirmtastatur für Touch -->
<div id="touchKeyboard" class="touch-keyboard" aria-hidden="true" role="application" aria-label="Bildschirmtastatur"></div>

<script>
const API_WECKER = 'api_wecker.php';
const API_EINSTELLUNGEN = 'api_einstellungen.php';
const API_UPDATE = 'api_update.php';
const API_INFO = 'api_info.php';
const API_MUSIK = 'api_musik.php';
const API_ICONS = 'api_icons.php';
const API_TTS = 'api_tts.php';
const API_WLAN  = 'api_wlan.php';
const API_STATS = 'api_stats.php';
const API_RADIO = 'api_radio.php';
var weckerListeData = <?= json_encode($wecker_fuer_js) ?>;
var _weckerLiveHash = JSON.stringify(weckerListeData);
const ZEITZONEN = <?= json_encode($zeitzonen) ?>;
var sleepModeSekunden = <?= (int)$sleep_mode_sekunden ?>;
window.weckerRemoteWsDebug = window.weckerRemoteWsDebug || {
    state: 'unbekannt',
    url: '',
    lastError: '',
    lastChange: 0
};

function setRemoteWsDebugState(patch) {
    var base = window.weckerRemoteWsDebug || {};
    var next = Object.assign({}, base, patch || {});
    next.lastChange = Date.now();
    window.weckerRemoteWsDebug = next;
    updateRemoteWsDebugStatus();
}

function updateRemoteWsDebugStatus() {
    var statusEl = document.getElementById('infoWsStatus');
    var metaEl = document.getElementById('infoWsMeta');
    var state = window.weckerRemoteWsDebug || {};
    if (statusEl) {
        statusEl.className = 'info-cron-status';
        if (state.state === 'connected') {
            statusEl.classList.add('status-ok');
            statusEl.textContent = 'Verbunden';
        } else if (state.state === 'connecting') {
            statusEl.classList.add('status-warn');
            statusEl.textContent = 'Verbinde…';
        } else if (state.state === 'error') {
            statusEl.classList.add('status-error');
            statusEl.textContent = 'Fehler';
        } else if (state.state === 'disconnected') {
            statusEl.classList.add('status-error');
            statusEl.textContent = 'Getrennt';
        } else {
            statusEl.textContent = 'Unbekannt';
        }
    }
    if (metaEl) {
        var msg = state.url ? ('URL: ' + state.url) : 'URL: –';
        if (state.lastChange) {
            msg += ' · zuletzt: ' + (new Date(state.lastChange)).toLocaleTimeString('de-DE');
        }
        if (state.lastError) {
            msg += ' · Fehler: ' + state.lastError;
        }
        metaEl.textContent = msg;
    }
}

/** Sleep-Mode nur auf localhost / 127.0.0.0/8 (nicht auf Handy/LAN) */
function isSleepModeHostErlaubt() {
    var h = (window.location.hostname || '').toLowerCase();
    return h === 'localhost' || h === '127.0.0.1' || /^127\.\d+\.\d+\.\d+$/.test(h);
}
var WOChentAGE_LABELS = { 1: 'Mo', 2: 'Di', 3: 'Mi', 4: 'Do', 5: 'Fr', 6: 'Sa', 7: 'So' };

/** QWERTZ-Layout für Bildschirmtastatur (Deutsch) */
var TOUCH_KEYBOARD_LAYOUT = [
    ['1','2','3','4','5','6','7','8','9','0','ß','´'],
    ['q','w','e','r','t','z','u','i','o','p','ü','+'],
    ['a','s','d','f','g','h','j','k','l','ö','ä','#'],
    ['Shift','z','x','c','v','b','n','m',',','.','-'],
    ['Space','Backspace','Fertig']
];

let audioList = [];
let zeitzoneListCloseTimer = null;

var touchKeyboardShift = false;
var touchKeyboardActiveEl = null;

function isTouchDevice() {
    return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (window.matchMedia && window.matchMedia('(pointer: coarse)').matches);
}

function buildTouchKeyboard() {
    var container = document.getElementById('touchKeyboard');
    if (!container || container.children.length > 0) return container;
    TOUCH_KEYBOARD_LAYOUT.forEach(function(row, rowIdx) {
        var rowEl = document.createElement('div');
        rowEl.className = 'touch-keyboard-row';
        row.forEach(function(k) {
            var btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'touch-keyboard-key';
            if (k === 'Space') {
                btn.className += ' touch-keyboard-key--space';
                btn.setAttribute('data-key', ' ');
                btn.innerHTML = '&nbsp;';
                btn.setAttribute('aria-label', 'Leertaste');
            } else if (k === 'Backspace') {
                btn.className += ' touch-keyboard-key--wide touch-keyboard-key--action';
                btn.setAttribute('data-key', 'Backspace');
                btn.textContent = 'Löschen';
                btn.setAttribute('aria-label', 'Rücktaste');
            } else if (k === 'Fertig') {
                btn.className += ' touch-keyboard-key--wide touch-keyboard-key--action';
                btn.setAttribute('data-key', 'Fertig');
                btn.textContent = 'Fertig';
                btn.setAttribute('aria-label', 'Tastatur schließen');
            } else if (k === 'Shift') {
                btn.className += ' touch-keyboard-key--wide touch-keyboard-key--action';
                btn.setAttribute('data-key', 'Shift');
                btn.textContent = 'Umschalt';
                btn.setAttribute('aria-label', 'Großschreibung');
            } else {
                btn.setAttribute('data-key', k);
                btn.textContent = k;
            }
            btn.addEventListener('click', function() { handleTouchKey(btn.getAttribute('data-key')); });
            rowEl.appendChild(btn);
        });
        container.appendChild(rowEl);
    });
    return container;
}

function handleTouchKey(key) {
    var el = document.activeElement;
    if (!el || (el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA')) return;
    if (key === 'Fertig') {
        hideTouchKeyboard();
        el.blur();
        if (el.readOnly !== undefined) el.readOnly = false;
        return;
    }
    if (key === 'Shift') {
        touchKeyboardShift = !touchKeyboardShift;
        var kb = document.getElementById('touchKeyboard');
        if (kb) {
            var shiftBtn = kb.querySelector('[data-key="Shift"]');
            if (shiftBtn) shiftBtn.classList.toggle('active', touchKeyboardShift);
        }
        return;
    }
    if (key === 'Backspace') {
        var start = el.selectionStart, end = el.selectionEnd;
        if (start === end && start > 0) {
            el.value = el.value.slice(0, start - 1) + el.value.slice(end);
            el.selectionStart = el.selectionEnd = start - 1;
        } else if (start !== end) {
            el.value = el.value.slice(0, start) + el.value.slice(end);
            el.selectionStart = el.selectionEnd = start;
        }
        el.dispatchEvent(new Event('input', { bubbles: true }));
        return;
    }
    var char = key;
    if (key.length === 1 && /[a-zäöüß]/.test(key) && touchKeyboardShift) {
        char = key.toUpperCase();
        if (key === 'ä') char = 'Ä';
        if (key === 'ö') char = 'Ö';
        if (key === 'ü') char = 'Ü';
        if (key === 'ß') char = 'ß';
    }
    var start = el.selectionStart, end = el.selectionEnd;
    el.value = el.value.slice(0, start) + char + el.value.slice(end);
    el.selectionStart = el.selectionEnd = start + char.length;
    el.dispatchEvent(new Event('input', { bubbles: true }));
    if (touchKeyboardShift && key.length === 1) {
        touchKeyboardShift = false;
        var kb = document.getElementById('touchKeyboard');
        if (kb) {
            var shiftBtn = kb.querySelector('[data-key="Shift"]');
            if (shiftBtn) shiftBtn.classList.remove('active');
        }
    }
}

function showTouchKeyboard(el) {
    if (!isSleepModeHostErlaubt()) return;
    var kb = document.getElementById('touchKeyboard');
    if (!kb || !el) return;
    touchKeyboardActiveEl = el;
    if (el.readOnly !== undefined) el.readOnly = true;
    kb.classList.add('visible');
    kb.setAttribute('aria-hidden', 'false');
    el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}

function hideTouchKeyboard() {
    var kb = document.getElementById('touchKeyboard');
    if (!kb) return;
    kb.classList.remove('visible');
    kb.setAttribute('aria-hidden', 'true');
    if (touchKeyboardActiveEl && touchKeyboardActiveEl.readOnly !== undefined) {
        touchKeyboardActiveEl.readOnly = false;
    }
    touchKeyboardActiveEl = null;
    touchKeyboardShift = false;
    var shiftBtn = kb.querySelector('[data-key="Shift"]');
    if (shiftBtn) shiftBtn.classList.remove('active');
}

function initTouchKeyboard() {
    if (!isSleepModeHostErlaubt() || !isTouchDevice()) return;
    buildTouchKeyboard();
    var selector = 'input[type="text"], input[type="number"], textarea';
    document.querySelectorAll(selector).forEach(function(input) {
        input.addEventListener('focus', function() {
            showTouchKeyboard(input);
        });
        input.addEventListener('blur', function() {
            setTimeout(function() {
                if (document.activeElement !== input && !document.getElementById('touchKeyboard').contains(document.activeElement)) {
                    hideTouchKeyboard();
                }
            }, 150);
            if (input.readOnly !== undefined) input.readOnly = false;
        }, false);
    });
    document.getElementById('touchKeyboard').addEventListener('mousedown', function(e) { e.preventDefault(); });
}

function initZeitzoneDropdown() {
    var input = document.getElementById('settingZeitzone');
    var list = document.getElementById('zeitzoneList');
    if (!input || !list) return;

    if (list.children.length === 0) {
        ZEITZONEN.forEach(function(z) {
            var li = document.createElement('li');
            li.setAttribute('role', 'option');
            li.textContent = z;
            li.dataset.value = z;
            li.addEventListener('click', function() {
                input.value = z;
                list.setAttribute('aria-hidden', 'true');
                input.setAttribute('aria-expanded', 'false');
            });
            list.appendChild(li);
        });
    }

    function filterList() {
        var q = (input.value || '').toLowerCase();
        var items = list.querySelectorAll('li');
        items.forEach(function(li) {
            var show = !q || li.dataset.value.toLowerCase().indexOf(q) !== -1;
            li.classList.toggle('zeitzone-hidden', !show);
        });
        list.setAttribute('aria-hidden', 'false');
        input.setAttribute('aria-expanded', 'true');
    }

    function closeList() {
        zeitzoneListCloseTimer = setTimeout(function() {
            list.setAttribute('aria-hidden', 'true');
            input.setAttribute('aria-expanded', 'false');
        }, 200);
    }

    input.addEventListener('focus', filterList);
    input.addEventListener('input', filterList);
    input.addEventListener('keydown', function(e) {
        if (e.key === 'Escape') { list.setAttribute('aria-hidden', 'true'); input.setAttribute('aria-expanded', 'false'); }
    });
    input.addEventListener('blur', closeList);
    list.addEventListener('mousedown', function(e) { e.preventDefault(); });
}

/** Wochentag-Toggle-Buttons: Klick/Touch synchronisiert die versteckte Checkbox und die aktiv-Klasse. */
function syncWochentageAktivKlasse() {
    document.querySelectorAll('#wochentageRow input[type="checkbox"]').forEach(function(cb) {
        var label = cb.closest('label');
        if (label) label.classList.toggle('tag-aktiv', cb.checked);
    });
}

function initWochentageToggle() {
    document.querySelectorAll('#wochentageRow label').forEach(function(label) {
        label.addEventListener('click', function(e) {
            e.preventDefault();
            var cb = label.querySelector('input[type="checkbox"]');
            if (!cb) return;
            cb.checked = !cb.checked;
            label.classList.toggle('tag-aktiv', cb.checked);
        });
    });
}

/** Platzhalter-Text an Cursorposition in der Sprachtext-Textarea einfügen (für Drop und Tap). */
function insertPlatzhalterInSprachtext(ta, text) {
    if (!ta || !text) return;
    ta.focus();
    var start = ta.selectionStart;
    var end = ta.selectionEnd;
    var val = ta.value || '';
    ta.value = val.slice(0, start) + text + val.slice(end);
    ta.selectionStart = ta.selectionEnd = start + text.length;
}

function initPlatzhalterDragDrop() {
    var ta = document.getElementById('editSprachtext');
    if (!ta) return;
    document.querySelectorAll('.platzhalter-chip').forEach(function(chip) {
        chip.addEventListener('dragstart', function(e) {
            e.dataTransfer.setData('text/plain', chip.getAttribute('data-placeholder') || '');
            e.dataTransfer.effectAllowed = 'copy';
        });
        chip.addEventListener('click', function(e) {
            var ph = chip.getAttribute('data-placeholder') || '';
            if (ph) insertPlatzhalterInSprachtext(ta, ph);
        });
    });
    ta.addEventListener('dragover', function(e) {
        e.preventDefault();
        e.dataTransfer.dropEffect = 'copy';
        ta.classList.add('editSprachtext-drop');
    });
    ta.addEventListener('dragleave', function(e) {
        if (!ta.contains(e.relatedTarget)) ta.classList.remove('editSprachtext-drop');
    });
    ta.addEventListener('drop', function(e) {
        e.preventDefault();
        ta.classList.remove('editSprachtext-drop');
        var text = e.dataTransfer.getData('text/plain');
        if (text) insertPlatzhalterInSprachtext(ta, text);
    });
}

function initSprachtextToggle() {
    var btnPlaceholder = document.getElementById('editSprachtextTogglePlaceholder');
    var btnPreview = document.getElementById('editSprachtextTogglePreview');
    var wrap = document.getElementById('editSprachtextWrap');
    var previewDiv = document.getElementById('editSprachtextPreview');
    var ta = document.getElementById('editSprachtext');
    if (!btnPlaceholder || !btnPreview || !wrap || !previewDiv || !ta) return;

    function showPlaceholder() {
        btnPlaceholder.classList.add('active');
        btnPlaceholder.setAttribute('aria-pressed', 'true');
        btnPreview.classList.remove('active');
        btnPreview.setAttribute('aria-pressed', 'false');
        wrap.style.display = '';
        previewDiv.style.display = 'none';
        previewDiv.setAttribute('aria-hidden', 'true');
    }

    function showPreview() {
        var text = (ta.value || '').trim();
        if (text === '') {
            previewDiv.textContent = '';
        } else {
            var fd = new FormData();
            fd.set('action', 'preview');
            fd.set('text', text);
            fd.set('id', document.getElementById('editId').value || '0');
            var weckzeit = document.getElementById('editWeckzeit').value;
            if (weckzeit) fd.set('weckzeit', weckzeit);
            previewDiv.textContent = 'Wird geladen…';
            fetch(API_WECKER, { method: 'POST', body: fd })
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    previewDiv.textContent = data.ok && data.preview !== undefined ? data.preview : (data.error || 'Fehler');
                })
                .catch(function() { previewDiv.textContent = 'Fehler beim Laden der Vorschau.'; });
        }
        btnPlaceholder.classList.remove('active');
        btnPlaceholder.setAttribute('aria-pressed', 'false');
        btnPreview.classList.add('active');
        btnPreview.setAttribute('aria-pressed', 'true');
        wrap.style.display = 'none';
        previewDiv.style.display = 'block';
        previewDiv.setAttribute('aria-hidden', 'false');
    }

    btnPlaceholder.addEventListener('click', function() { showPlaceholder(); });
    btnPreview.addEventListener('click', function() { showPreview(); });

    window.sprachtextToggleShowPlaceholder = showPlaceholder;
}

(function initTtsGeneratePlay() {
    var btn = document.getElementById('editTtsGeneratePlay');
    var statusEl = document.getElementById('editTtsGenerateStatus');
    if (!btn || !statusEl) return;
    btn.addEventListener('click', function() {
        var text = (document.getElementById('editSprachtext') || {}).value;
        if (!text || !String(text).trim()) {
            statusEl.textContent = 'Bitte zuerst Sprachtext eingeben.';
            return;
        }
        var weckzeit = (document.getElementById('editWeckzeit') || {}).value || '';
        var id = (document.getElementById('editId') || {}).value || '0';
        statusEl.textContent = 'TTS wird erzeugt…';
        btn.disabled = true;
        var fd = new FormData();
        fd.set('action', 'generate');
        fd.set('text', text.trim());
        fd.set('weckzeit', weckzeit);
        fd.set('id', id);
        fetch(API_TTS, { method: 'POST', body: fd })
            .then(function(r) { return r.json(); })
            .then(function(data) {
                btn.disabled = false;
                if (!data.ok || !data.url) {
                    statusEl.textContent = data.error || 'TTS-Generierung fehlgeschlagen.';
                    return;
                }
                statusEl.textContent = 'Wird abgespielt…';
                var audio = new Audio(data.url);
                audio.play().then(function() {
                    statusEl.textContent = '';
                }).catch(function() {
                    statusEl.textContent = 'Abspielen fehlgeschlagen.';
                });
                audio.addEventListener('ended', function() {
                    statusEl.textContent = '';
                });
            })
            .catch(function() {
                btn.disabled = false;
                statusEl.textContent = 'Netzwerkfehler.';
            });
    });
})();

function getTheme() {
    var t = document.documentElement.getAttribute('data-theme');
    return (t === 'light' || t === 'dark') ? t : 'light';
}

function setTheme(theme) {
    theme = theme === 'dark' ? 'dark' : 'light';
    document.documentElement.setAttribute('data-theme', theme);
    localStorage.setItem('wecker-theme', theme);
    var meta = document.getElementById('metaThemeColor');
    if (meta) meta.content = theme === 'dark' ? '#0d1117' : '#ffffff';
    updateThemeButtons(theme);
}
function updateThemeButtons(theme) {
    var icon = theme === 'dark' ? '/icon.php?name=theme-sun.svg' : '/icon.php?name=theme-moon.svg';
    var alt = theme === 'dark' ? 'Hell' : 'Dunkel';
    ['themeToggle', 'themeToggle4', 'themeToggle5', 'themeToggleMusik'].forEach(function(id) {
        var btn = document.getElementById(id);
        if (btn) {
            btn.innerHTML = '<img src="' + icon + '" alt="' + alt + '" class="theme-icon" width="22" height="22">';
        }
    });
}

function toggleTheme() {
    setTheme(getTheme() === 'dark' ? 'light' : 'dark');
}

(function updateThemeButton() {
    updateThemeButtons(getTheme());
})();

// ── Musik-Player ─────────────────────────────────────────────────────────────
var musikAudio = new Audio();
var musikAllTracks = [];      // alle geladenen Tracks
var musikFilteredTracks = []; // nach Playlist-Filter
var musikCurrentIndex = -1;   // Index in musikFilteredTracks
var musikCurrentFilter = 'all';
var musikSeekDragging = false;
var musikCurrentTrack = null;
var musikCurrentRadioIndex = -1;
var musikRadioAllTracks = [];
var musikRadioFilteredTracks = [];
var musikRadioLoaded = false;
var musikCurrentSubtab = 'lokal';

function musikIsLiveTrack(track) {
    return !!track && track.source_type === 'radio';
}

function musikFmtTime(sec) {
    sec = Math.floor(sec || 0);
    var m = Math.floor(sec / 60);
    var s = sec % 60;
    return m + ':' + (s < 10 ? '0' : '') + s;
}

function musikCoverPlaceholder() {
    return 'data:image/svg+xml,' + encodeURIComponent(
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#888" stroke-width="1.5">' +
        '<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>' +
        '</svg>'
    );
}

// ── Tracks laden & rendern ────────────────────────────────────────────────────
function loadMusikListe() {
    fetch(API_MUSIK + '?action=list')
        .then(function(r) { return r.json(); })
        .then(function(data) {
            musikAllTracks = (data.ok && data.files) ? data.files : [];
            window.audioSearchDataLoaded = false;
            musikApplyFilter(musikCurrentFilter);
        })
        .catch(function() {
            musikAllTracks = [];
            musikApplyFilter('all');
        });
}

function musikApplyFilter(filter) {
    musikCurrentFilter = filter;
    document.querySelectorAll('.musik-filter-btn').forEach(function(b) {
        b.classList.toggle('active', b.dataset.playlistFilter === filter);
    });
    if (filter === 'all') {
        musikFilteredTracks = musikAllTracks.slice();
    } else {
        musikFilteredTracks = musikAllTracks.filter(function(t) {
            return t.playlist_ids && t.playlist_ids.indexOf(String(filter)) !== -1;
        });
    }
    musikRenderTracklist();
}

function musikRenderTracklist() {
    var ul = document.getElementById('musikTracklist');
    var leer = document.getElementById('musikListeLeer');
    if (!ul) return;
    ul.innerHTML = '';
    if (musikFilteredTracks.length === 0) {
        if (leer) leer.style.display = 'block';
        return;
    }
    if (leer) leer.style.display = 'none';
    var ph = musikCoverPlaceholder();
    musikFilteredTracks.forEach(function(f, i) {
        var li = document.createElement('li');
        li.className = 'musik-track-item' + (i === musikCurrentIndex ? ' playing' : '');
        li.dataset.trackIdx = i;
        var coverUrl = '/audio_cover.php?f=' + encodeURIComponent(f.name);
        var displayName = (f.titel && f.titel.trim()) ? f.titel.trim() : f.name;
        var playlistStr = (f.playlists && String(f.playlists).trim()) ? escapeHtml(String(f.playlists).trim()) : '';
        var playlistIds = (f.playlist_ids && Array.isArray(f.playlist_ids)) ? f.playlist_ids.join(',') : '';
        var actionsHtml =
            '<button type="button" class="player-btn" style="padding:5px;border-radius:6px" ' +
                'data-musik-edit="' + f.id + '" ' +
                'data-musik-titel="' + escapeHtml(displayName) + '" ' +
                'data-musik-playlist-ids="' + escapeHtml(playlistIds) + '" ' +
                'title="Bearbeiten" aria-label="Bearbeiten">' +
                '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">' +
                    '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>' +
                    '<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>' +
                '</svg>' +
            '</button>' +
            '<button type="button" class="player-btn" style="padding:5px;border-radius:6px;color:var(--danger)" ' +
                'data-musik-delete="' + escapeHtml(f.name) + '" ' +
                'title="L\u00f6schen" aria-label="L\u00f6schen">' +
                '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">' +
                    '<polyline points="3 6 5 6 21 6"/>' +
                    '<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>' +
                    '<path d="M10 11v6"/><path d="M14 11v6"/>' +
                    '<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>' +
                '</svg>' +
            '</button>';
        li.innerHTML =
            '<div class="track-cover">' +
                '<img src="' + coverUrl + '" alt="" loading="lazy" onerror="this.onerror=null;this.src=\'' + ph + '\'">' +
                '<div class="track-cover-playing">' +
                    '<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
                        '<rect x="4" y="4" width="5" height="16" rx="1"/><rect x="15" y="4" width="5" height="16" rx="1"/>' +
                    '</svg>' +
                '</div>' +
            '</div>' +
            '<div class="track-info">' +
                '<div class="track-title">' + escapeHtml(displayName) + '</div>' +
                (playlistStr ? '<div class="track-meta">' + playlistStr + '</div>' : '') +
            '</div>' +
            '<div class="track-actions">' +
                actionsHtml +
            '</div>';
        ul.appendChild(li);
    });
}

function musikNormalizeRadioStations(stations) {
    var out = [];
    (stations || []).forEach(function(s) {
        var name = String((s && s.name) || '').trim();
        var streamUrl = String((s && s.stream_url) || '').trim();
        if (!name || !streamUrl) return;
        out.push({
            id: String((s && s.id) || ''),
            name: name,
            titel: name,
            url: streamUrl,
            source_type: 'radio',
            playlists: 'Radio Browser DE',
            favicon: String((s && s.favicon) || '').trim() || null,
            tags: String((s && s.tags) || '').trim() || ''
        });
    });
    return out;
}

function loadRadioStations(cb) {
    fetch(API_RADIO + '?action=list_de')
        .then(function(r) { return r.json(); })
        .then(function(data) {
            musikRadioAllTracks = (data && data.ok && Array.isArray(data.stations))
                ? musikNormalizeRadioStations(data.stations)
                : [];
            musikRadioLoaded = true;
            musikFilterRadioStations((document.getElementById('musikRadioSearch') || {}).value || '');
            if (cb) cb();
        })
        .catch(function() {
            musikRadioAllTracks = [];
            musikRadioLoaded = true;
            musikFilterRadioStations((document.getElementById('musikRadioSearch') || {}).value || '');
            if (cb) cb();
        });
}

function musikFilterRadioStations(query) {
    var q = String(query || '').trim().toLowerCase();
    if (!q) {
        musikRadioFilteredTracks = musikRadioAllTracks.slice();
    } else {
        musikRadioFilteredTracks = musikRadioAllTracks.filter(function(s) {
            var hay = (s.name + ' ' + (s.tags || '')).toLowerCase();
            return hay.indexOf(q) !== -1;
        });
    }
    musikRenderRadioTracklist();
}

function musikRenderRadioTracklist() {
    var ul = document.getElementById('musikRadioTracklist');
    var leer = document.getElementById('musikRadioLeer');
    if (!ul) return;
    ul.innerHTML = '';
    if (musikRadioFilteredTracks.length === 0) {
        if (leer) leer.style.display = 'block';
        return;
    }
    if (leer) leer.style.display = 'none';
    var ph = musikCoverPlaceholder();
    musikRadioFilteredTracks.forEach(function(station, i) {
        var li = document.createElement('li');
        li.className = 'musik-track-item radio-track-item' + (i === musikCurrentRadioIndex ? ' playing' : '');
        li.dataset.radioIdx = i;
        var favicon = station.favicon || ph;
        var tags = station.tags ? escapeHtml(station.tags) : 'DE';
        li.innerHTML =
            '<div class="track-cover">' +
                '<img src="' + escapeHtml(favicon) + '" alt="" loading="lazy" referrerpolicy="no-referrer" onerror="this.onerror=null;this.src=\'' + ph + '\'">' +
                '<div class="track-cover-playing">' +
                    '<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
                        '<rect x="4" y="4" width="5" height="16" rx="1"/><rect x="15" y="4" width="5" height="16" rx="1"/>' +
                    '</svg>' +
                '</div>' +
            '</div>' +
            '<div class="track-info">' +
                '<div class="track-title">' + escapeHtml(station.name) + '</div>' +
                '<div class="track-meta">Radio Browser DE' + (tags ? ' · ' + tags : '') + '</div>' +
            '</div>';
        ul.appendChild(li);
    });
}

function musikPlayRadioTrack(index) {
    if (index < 0 || index >= musikRadioFilteredTracks.length) return;
    musikCurrentRadioIndex = index;
    musikCurrentIndex = -1;
    musikHighlightTrack(-1);
    var station = musikRadioFilteredTracks[index];
    musikCurrentTrack = station;
    musikAudio.src = station.url;
    musikAudio.play().catch(function() {});
    var seekbarLive = document.getElementById('playerSeekbar');
    var curLive = document.getElementById('playerTimeCurrent');
    var durLive = document.getElementById('playerTimeDuration');
    if (seekbarLive) { seekbarLive.value = 0; seekbarLive.disabled = true; }
    if (curLive) curLive.textContent = 'LIVE';
    if (durLive) durLive.textContent = 'LIVE';
    musikUpdatePlayerUI(station);
    musikRenderRadioTracklist();
    musikUpdateSleepModePlayerUI();
    var sub = document.getElementById('musikSubtitle');
    if (sub) sub.textContent = station.name;
}

function musikSwitchSubtab(tab) {
    musikCurrentSubtab = tab === 'radio' ? 'radio' : 'lokal';
    var localPane = document.getElementById('musikTabLokal');
    var radioPane = document.getElementById('musikTabRadio');
    if (localPane) localPane.classList.toggle('active', musikCurrentSubtab === 'lokal');
    if (radioPane) radioPane.classList.toggle('active', musikCurrentSubtab === 'radio');
    document.querySelectorAll('.musik-subtab-btn').forEach(function(btn) {
        btn.classList.toggle('active', btn.dataset.musikTab === musikCurrentSubtab);
    });
    if (musikCurrentSubtab === 'radio' && !musikRadioLoaded) {
        loadRadioStations();
    }
}

// ── Playback ──────────────────────────────────────────────────────────────────
function musikPlayTrack(index) {
    if (index < 0 || index >= musikFilteredTracks.length) return;
    musikCurrentIndex = index;
    musikCurrentRadioIndex = -1;
    var track = musikFilteredTracks[index];
    musikCurrentTrack = track;
    musikAudio.src = track.url;
    musikAudio.play().catch(function() {});
    if (musikIsLiveTrack(track)) {
        var seekbarLive = document.getElementById('playerSeekbar');
        var curLive = document.getElementById('playerTimeCurrent');
        var durLive = document.getElementById('playerTimeDuration');
        if (seekbarLive) { seekbarLive.value = 0; seekbarLive.disabled = true; }
        if (curLive) curLive.textContent = 'LIVE';
        if (durLive) durLive.textContent = 'LIVE';
    } else {
        var seekbarAudio = document.getElementById('playerSeekbar');
        if (seekbarAudio) seekbarAudio.disabled = false;
    }
    musikUpdatePlayerUI(track);
    musikHighlightTrack(index);
    musikRenderRadioTracklist();
    musikUpdateSleepModePlayerUI();
    // Subtitle aktualisieren
    var displayName = (track.titel && track.titel.trim()) ? track.titel.trim() : track.name;
    var sub = document.getElementById('musikSubtitle');
    if (sub) sub.textContent = displayName;
}

function musikUpdatePlayerUI(track) {
    var titleEl = document.getElementById('playerTitle');
    var playlistEl = document.getElementById('playerPlaylistName');
    var img = document.getElementById('playerCoverImg');
    var fallback = document.getElementById('playerCoverFallback');
    if (!track) {
        if (titleEl) titleEl.textContent = 'Kein Titel';
        if (playlistEl) playlistEl.textContent = '\u2014';
        if (img) img.style.display = 'none';
        if (fallback) fallback.style.display = 'flex';
        return;
    }
    var displayName = (track.titel && track.titel.trim()) ? track.titel.trim() : track.name;
    if (titleEl) titleEl.textContent = displayName;
    var pl = musikIsLiveTrack(track)
        ? 'Radio'
        : ((track.playlists && String(track.playlists).trim()) ? String(track.playlists).trim() : '\u2014');
    if (playlistEl) playlistEl.textContent = pl;
    if (img && fallback) {
        if (musikIsLiveTrack(track)) {
            var radioFavicon = track.favicon ? String(track.favicon) : '';
            if (radioFavicon !== '') {
                img.onload = function() { img.style.display = ''; fallback.style.display = 'none'; };
                img.onerror = function() { img.style.display = 'none'; fallback.style.display = 'flex'; };
                img.src = radioFavicon;
            } else {
                img.removeAttribute('src');
                img.style.display = 'none';
                fallback.style.display = 'flex';
            }
            return;
        }
        img.onload = function() { img.style.display = ''; fallback.style.display = 'none'; };
        img.onerror = function() { img.style.display = 'none'; fallback.style.display = 'flex'; };
        img.src = '/audio_cover.php?f=' + encodeURIComponent(track.name);
    }
    musikUpdateSleepModePlayerUI();
}

function musikHighlightTrack(index) {
    document.querySelectorAll('.musik-track-item').forEach(function(el, i) {
        el.classList.toggle('playing', i === index);
    });
}

function musikUpdatePlayPauseIcon() {
    var icon = document.getElementById('playerPlayIcon');
    var sleepIcon = document.getElementById('sleepPlayerPlayIcon');
    var html = musikAudio.paused
        ? '<polygon points="5,3 19,12 5,21"/>'
        : '<rect x="4" y="4" width="5" height="16" rx="1"/><rect x="15" y="4" width="5" height="16" rx="1"/>';
    if (icon) icon.innerHTML = html;
    if (sleepIcon) sleepIcon.innerHTML = html;
}

function musikUpdateSleepModePlayerUI() {
    var playerWrap = document.getElementById('sleepMusicPlayer');
    var titleEl = document.getElementById('sleepPlayerTitle');
    var metaEl = document.getElementById('sleepPlayerMeta');
    var coverImg = document.getElementById('sleepPlayerCoverImg');
    var coverFallback = document.getElementById('sleepPlayerCoverFallback');
    var slider = document.getElementById('sleepPlayerVolumeSlider');
    if (!playerWrap || !titleEl || !metaEl || !coverImg || !coverFallback || !slider) return;

    var track = musikCurrentTrack;
    if (!track) {
        playerWrap.style.display = 'none';
        titleEl.textContent = 'Kein Lied ausgewählt';
        metaEl.textContent = 'Bereit';
        coverImg.removeAttribute('src');
        coverImg.style.display = 'none';
        coverFallback.style.display = 'flex';
    } else {
        playerWrap.style.display = '';
        var displayName = (track.titel && track.titel.trim()) ? track.titel.trim() : track.name;
        titleEl.textContent = displayName;
        if (musikIsLiveTrack(track)) {
            metaEl.textContent = 'Radio · LIVE';
            coverImg.removeAttribute('src');
            coverImg.style.display = 'none';
            coverFallback.style.display = 'flex';
        } else {
            var pl = (track.playlists && String(track.playlists).trim()) ? String(track.playlists).trim() : 'Lokale Musik';
            metaEl.textContent = pl;
            coverImg.onload = function() { coverImg.style.display = ''; coverFallback.style.display = 'none'; };
            coverImg.onerror = function() { coverImg.style.display = 'none'; coverFallback.style.display = 'flex'; };
            coverImg.src = '/audio_cover.php?f=' + encodeURIComponent(track.name);
        }
    }

    slider.value = String(Math.round((musikAudio.volume || 0) * 100));
    musikUpdatePlayPauseIcon();
    musikUpdateVolumeIcon();
}

// ── Audio Events ──────────────────────────────────────────────────────────────
musikAudio.addEventListener('timeupdate', function() {
    if (musikIsLiveTrack(musikCurrentTrack)) {
        var curLive = document.getElementById('playerTimeCurrent');
        var durLive = document.getElementById('playerTimeDuration');
        if (curLive) curLive.textContent = 'LIVE';
        if (durLive) durLive.textContent = 'LIVE';
        return;
    }
    if (musikSeekDragging) return;
    var seekbar = document.getElementById('playerSeekbar');
    var cur = document.getElementById('playerTimeCurrent');
    if (seekbar && musikAudio.duration) {
        seekbar.value = (musikAudio.currentTime / musikAudio.duration) * 100;
    }
    if (cur) cur.textContent = musikFmtTime(musikAudio.currentTime);
});

musikAudio.addEventListener('loadedmetadata', function() {
    var dur = document.getElementById('playerTimeDuration');
    var seekbar = document.getElementById('playerSeekbar');
    if (musikIsLiveTrack(musikCurrentTrack)) {
        if (dur) dur.textContent = 'LIVE';
        if (seekbar) { seekbar.value = 0; seekbar.disabled = true; }
        return;
    }
    if (dur) dur.textContent = musikFmtTime(musikAudio.duration);
    if (seekbar) { seekbar.value = 0; seekbar.disabled = false; }
});

musikAudio.addEventListener('ended', function() {
    var next = musikCurrentIndex + 1;
    if (next < musikFilteredTracks.length) {
        musikPlayTrack(next);
    } else {
        musikCurrentIndex = -1;
        musikCurrentTrack = null;
        var sub = document.getElementById('musikSubtitle');
        if (sub) sub.textContent = 'Bereit';
        musikUpdatePlayPauseIcon();
        musikUpdateSleepModePlayerUI();
    }
});

musikAudio.addEventListener('play', musikUpdatePlayPauseIcon);
musikAudio.addEventListener('pause', musikUpdatePlayPauseIcon);

// ── Alben laden ───────────────────────────────────────────────────────────────
function loadAlbenListe() {
    var ul = document.getElementById('musikAlbenListe');
    var filterBar = document.getElementById('musikFilterBar');
    var uploadWrap = document.getElementById('musikUploadPlaylistWrap');
    if (ul) ul.innerHTML = '';
    // Filter-Bar: statische Buttons behalten, dynamische Playlist-Buttons entfernen
    if (filterBar) {
        filterBar.querySelectorAll('.musik-filter-btn[data-dynamic="1"]').forEach(function(btn) {
            btn.remove();
        });
    }
    fetch(API_MUSIK + '?action=alben_list')
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (!data.ok || !data.alben) return;
            window.musikPlaylists = data.alben;
            window.audioSearchDataLoaded = false;
            data.alben.forEach(function(a) {
                // Filter-Button
                if (filterBar) {
                    var btn = document.createElement('button');
                    btn.className = 'musik-filter-btn';
                    btn.dataset.playlistFilter = String(a.id);
                    btn.dataset.dynamic = '1';
                    btn.textContent = a.name;
                    filterBar.appendChild(btn);
                }
                // Verwaltungs-Liste
                if (ul) {
                    var li = document.createElement('li');
                    var coverUrl = '/album_cover.php?album_id=' + a.id + '&t=' + Date.now();
                    var hasIcon = a.icon ? '1' : '0';
                    li.innerHTML =
                        '<div class="album-cover-wrap"><img src="' + coverUrl + '" alt="" width="40" height="40" loading="lazy" onerror="this.style.display=\'none\'"></div>' +
                        '<span>' + escapeHtml(a.name) + '</span>' +
                        '<button type="button" class="header-btn" data-album-edit="' + a.id + '" data-album-name="' + escapeHtml(a.name) + '" data-album-has-icon="' + hasIcon + '" aria-label="Playlist bearbeiten">Bearbeiten</button>' +
                        '<button type="button" class="header-btn" data-album-delete="' + a.id + '" aria-label="Playlist l\u00f6schen">L\u00f6schen</button>';
                    ul.appendChild(li);
                }
                // Upload-Overlay Checkboxen
                if (uploadWrap) {
                    var label = document.createElement('label');
                    label.innerHTML = '<input type="checkbox" name="upload_playlist" value="' + a.id + '"> ' + escapeHtml(a.name);
                    uploadWrap.appendChild(label);
                }
            });
            // Aktiven Filter-Button korrekt markieren
            if (filterBar) {
                filterBar.querySelectorAll('.musik-filter-btn').forEach(function(b) {
                    b.classList.toggle('active', b.dataset.playlistFilter === musikCurrentFilter);
                });
            }
        });
}

// ── initMusik ─────────────────────────────────────────────────────────────────
function initMusik() {
    // Lautstärke initialisieren
    var volSlider = document.getElementById('playerVolumeSlider');
    if (volSlider) musikAudio.volume = volSlider.value / 100;

    // Play/Pause
    var playBtn = document.getElementById('playerPlayPause');
    if (playBtn) {
        playBtn.addEventListener('click', function() {
            if (musikCurrentTrack === null) {
                if (musikCurrentSubtab === 'radio') {
                    if (musikRadioFilteredTracks.length > 0) musikPlayRadioTrack(0);
                    return;
                }
                if (musikFilteredTracks.length > 0) musikPlayTrack(0);
                return;
            }
            if (musikAudio.paused) musikAudio.play().catch(function() {});
            else musikAudio.pause();
        });
    }

    // Prev / Next
    var prevBtn = document.getElementById('playerPrev');
    if (prevBtn) {
        prevBtn.addEventListener('click', function() {
            if (musikIsLiveTrack(musikCurrentTrack)) {
                if (musikRadioFilteredTracks.length === 0) return;
                var rIdx = musikCurrentRadioIndex > 0 ? musikCurrentRadioIndex - 1 : musikRadioFilteredTracks.length - 1;
                musikPlayRadioTrack(rIdx);
                return;
            }
            var idx = musikCurrentIndex > 0 ? musikCurrentIndex - 1 : musikFilteredTracks.length - 1;
            if (musikFilteredTracks.length > 0) musikPlayTrack(idx);
        });
    }
    var nextBtn = document.getElementById('playerNext');
    if (nextBtn) {
        nextBtn.addEventListener('click', function() {
            if (musikIsLiveTrack(musikCurrentTrack)) {
                if (musikRadioFilteredTracks.length === 0) return;
                var rIdx = musikCurrentRadioIndex < musikRadioFilteredTracks.length - 1 ? musikCurrentRadioIndex + 1 : 0;
                musikPlayRadioTrack(rIdx);
                return;
            }
            var idx = musikCurrentIndex < musikFilteredTracks.length - 1 ? musikCurrentIndex + 1 : 0;
            if (musikFilteredTracks.length > 0) musikPlayTrack(idx);
        });
    }

    // Seekbar
    var seekbar = document.getElementById('playerSeekbar');
    if (seekbar) {
        seekbar.addEventListener('mousedown', function() { musikSeekDragging = true; });
        seekbar.addEventListener('touchstart', function() { musikSeekDragging = true; }, { passive: true });
        seekbar.addEventListener('input', function() {
            if (musikIsLiveTrack(musikCurrentTrack)) return;
            var cur = document.getElementById('playerTimeCurrent');
            if (musikAudio.duration) {
                var t = (seekbar.value / 100) * musikAudio.duration;
                if (cur) cur.textContent = musikFmtTime(t);
            }
        });
        seekbar.addEventListener('change', function() {
            musikSeekDragging = false;
            if (musikIsLiveTrack(musikCurrentTrack)) return;
            if (musikAudio.duration) musikAudio.currentTime = (seekbar.value / 100) * musikAudio.duration;
        });
        seekbar.addEventListener('mouseup', function() { musikSeekDragging = false; });
    }

    // Lautstärke
    if (volSlider) {
        volSlider.addEventListener('input', function() {
            musikAudio.volume = volSlider.value / 100;
            musikAudio.muted = false;
            musikUpdateVolumeIcon();
        });
    }
    var volBtn = document.getElementById('playerVolumeBtn');
    if (volBtn) {
        volBtn.addEventListener('click', function() {
            musikAudio.muted = !musikAudio.muted;
            musikUpdateVolumeIcon();
        });
    }

    // Filter-Bar (Event Delegation)
    var filterBar = document.getElementById('musikFilterBar');
    if (filterBar) {
        filterBar.addEventListener('click', function(e) {
            var btn = e.target.closest('.musik-filter-btn');
            if (!btn) return;
            var f = btn.dataset.playlistFilter;
            if (f !== undefined) musikApplyFilter(f);
        });
    }
    var subTabs = document.getElementById('musikSubtabs');
    if (subTabs) {
        subTabs.addEventListener('click', function(e) {
            var btn = e.target.closest('.musik-subtab-btn');
            if (!btn) return;
            musikSwitchSubtab(btn.dataset.musikTab || 'lokal');
        });
    }

    // Tracklist klicken (play / edit / delete)
    var tracklist = document.getElementById('musikTracklist');
    if (tracklist) {
        tracklist.addEventListener('click', function(e) {
            var t = e.target;
            var delName = t && t.closest('[data-musik-delete]') && t.closest('[data-musik-delete]').getAttribute('data-musik-delete');
            var editBtn = t && t.closest('[data-musik-edit]');
            if (delName || (t && t.getAttribute('data-musik-delete'))) {
                var name = delName || t.getAttribute('data-musik-delete');
                if (!confirm('Datei \u201e' + name + '\u201c wirklich l\u00f6schen?')) return;
                var fd = new FormData();
                fd.append('action', 'delete');
                fd.append('name', name);
                fetch(API_MUSIK, { method: 'POST', body: fd })
                    .then(function(r) { return r.json(); })
                    .then(function(data) { if (data.ok) loadMusikListe(); });
                return;
            }
            if (editBtn) {
                var editOverlay = document.getElementById('musikEditOverlay');
                var idInp = document.getElementById('musikEditId');
                var titelInp = document.getElementById('musikEditTitel');
                var playlistsWrap = document.getElementById('musikEditPlaylists');
                if (!editOverlay || !idInp || !titelInp) return;
                idInp.value = editBtn.getAttribute('data-musik-edit');
                titelInp.value = editBtn.getAttribute('data-musik-titel') || '';
                var idsStr = editBtn.getAttribute('data-musik-playlist-ids') || '';
                var checkedIds = idsStr ? idsStr.split(',').map(function(x) { return x.trim(); }).filter(Boolean) : [];
                function fillPlaylistsAndShow() {
                    if (playlistsWrap && window.musikPlaylists && Array.isArray(window.musikPlaylists)) {
                        playlistsWrap.innerHTML = '';
                        window.musikPlaylists.forEach(function(p) {
                            var label = document.createElement('label');
                            var checked = checkedIds.indexOf(String(p.id)) !== -1;
                            label.innerHTML = '<input type="checkbox" name="edit_playlist" value="' + p.id + '"' + (checked ? ' checked' : '') + '> ' + escapeHtml(p.name);
                            playlistsWrap.appendChild(label);
                        });
                    }
                    editOverlay.setAttribute('aria-hidden', 'false');
                }
                if (window.musikPlaylists && Array.isArray(window.musikPlaylists) && window.musikPlaylists.length > 0) {
                    fillPlaylistsAndShow();
                } else {
                    fetch(API_MUSIK + '?action=alben_list')
                        .then(function(r) { return r.json(); })
                        .then(function(data) {
                            if (data.ok && data.alben && data.alben.length) window.musikPlaylists = data.alben;
                            else window.musikPlaylists = [];
                            fillPlaylistsAndShow();
                        })
                        .catch(function() { window.musikPlaylists = []; fillPlaylistsAndShow(); });
                }
                return;
            }
            // Klick auf Track-Item → abspielen
            var item = t && t.closest('.musik-track-item');
            if (item && item.dataset.trackIdx !== undefined) {
                musikPlayTrack(parseInt(item.dataset.trackIdx, 10));
            }
        });
    }
    var radioTracklist = document.getElementById('musikRadioTracklist');
    if (radioTracklist) {
        radioTracklist.addEventListener('click', function(e) {
            var item = e.target && e.target.closest('.radio-track-item');
            if (!item || item.dataset.radioIdx === undefined) return;
            musikPlayRadioTrack(parseInt(item.dataset.radioIdx, 10));
        });
    }
    var radioSearch = document.getElementById('musikRadioSearch');
    if (radioSearch) {
        radioSearch.addEventListener('focus', function() {
            if (!musikRadioLoaded) loadRadioStations();
        });
        radioSearch.addEventListener('input', function() {
            musikFilterRadioStations(radioSearch.value || '');
        });
    }

    // Song-Edit speichern
    var editOverlay = document.getElementById('musikEditOverlay');
    var editCancel = document.getElementById('musikEditCancel');
    var editSave = document.getElementById('musikEditSave');
    if (editCancel && editOverlay) editCancel.addEventListener('click', function() { editOverlay.setAttribute('aria-hidden', 'true'); });
    if (editSave && editOverlay) {
        editSave.addEventListener('click', function() {
            var idInp = document.getElementById('musikEditId');
            var titelInp = document.getElementById('musikEditTitel');
            var playlistsWrap = document.getElementById('musikEditPlaylists');
            if (!idInp || !titelInp) return;
            var fd = new FormData();
            fd.append('action', 'update');
            fd.append('id', idInp.value);
            fd.append('titel', titelInp.value.trim());
            if (playlistsWrap) {
                playlistsWrap.querySelectorAll('input[name="edit_playlist"]:checked').forEach(function(cb) {
                    fd.append('playlist_ids[]', cb.value);
                });
            }
            fetch(API_MUSIK, { method: 'POST', body: fd })
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    if (data.ok) { editOverlay.setAttribute('aria-hidden', 'true'); loadMusikListe(); }
                    else if (data.error) { alert(data.error); }
                })
                .catch(function() { alert('Speichern fehlgeschlagen.'); });
        });
    }

    // Hochladen-Button → Upload-Overlay
    var uploadBtn = document.getElementById('musikUploadBtn');
    var uploadOverlay = document.getElementById('musikUploadOverlay');
    var uploadCancel = document.getElementById('musikUploadCancel');
    var uploadStart = document.getElementById('musikUploadStart');
    var fileInput = document.getElementById('musikFileInput');
    var statusEl = document.getElementById('musikUploadStatus');
    if (uploadBtn && uploadOverlay) {
        uploadBtn.addEventListener('click', function() {
            var wrap = document.getElementById('musikUploadPlaylistWrap');
            if (wrap) {
                wrap.innerHTML = '';
                if (window.musikPlaylists && Array.isArray(window.musikPlaylists)) {
                    window.musikPlaylists.forEach(function(p) {
                        var label = document.createElement('label');
                        label.innerHTML = '<input type="checkbox" name="upload_playlist" value="' + p.id + '"> ' + escapeHtml(p.name);
                        wrap.appendChild(label);
                    });
                }
            }
            uploadOverlay.setAttribute('aria-hidden', 'false');
        });
    }
    if (uploadCancel && uploadOverlay) uploadCancel.addEventListener('click', function() { uploadOverlay.setAttribute('aria-hidden', 'true'); });
    if (uploadStart && fileInput) {
        uploadStart.addEventListener('click', function() {
            uploadOverlay.setAttribute('aria-hidden', 'true');
            fileInput.click();
        });
    }
    if (fileInput) {
        fileInput.addEventListener('change', function() {
            var files = fileInput.files;
            if (!files || files.length === 0) return;
            var wrap = document.getElementById('musikUploadPlaylistWrap');
            var playlistIds = [];
            if (wrap) {
                wrap.querySelectorAll('input[name="upload_playlist"]:checked').forEach(function(cb) { playlistIds.push(cb.value); });
            }
            var total = files.length, done = 0, failed = 0;
            function uploadNext(i) {
                if (i >= total) {
                    fileInput.value = '';
                    loadMusikListe();
                    if (statusEl) statusEl.textContent = failed
                        ? done + ' hochgeladen, ' + failed + ' Fehler.'
                        : (total === 1 ? 'Hochgeladen.' : total + ' Dateien hochgeladen.');
                    return;
                }
                if (statusEl) statusEl.textContent = 'L\u00e4dt hoch \u2026 ' + (i + 1) + '/' + total;
                var fd = new FormData();
                fd.append('file', files[i]);
                fd.append('action', 'upload');
                playlistIds.forEach(function(pid) { fd.append('playlist_ids[]', pid); });
                fetch(API_MUSIK, { method: 'POST', body: fd })
                    .then(function(r) { return r.json(); })
                    .then(function(data) { if (data.ok) done++; else failed++; uploadNext(i + 1); })
                    .catch(function() { failed++; uploadNext(i + 1); });
            }
            uploadNext(0);
        });
    }

    // Playlists verwalten
    var verwaltenBtn = document.getElementById('musikVerwaltenBtn');
    var manageOverlay = document.getElementById('musikManageOverlay');
    var manageClose = document.getElementById('musikManageClose');
    if (verwaltenBtn && manageOverlay) {
        verwaltenBtn.addEventListener('click', function() { manageOverlay.setAttribute('aria-hidden', 'false'); });
    }
    if (manageClose && manageOverlay) {
        manageClose.addEventListener('click', function() { manageOverlay.setAttribute('aria-hidden', 'true'); });
    }

    // Playlist erstellen
    var albumErstellenBtn = document.getElementById('musikAlbumErstellen');
    var albumNeuInput = document.getElementById('musikAlbumNeu');
    if (albumErstellenBtn && albumNeuInput) {
        albumErstellenBtn.addEventListener('click', function() {
            var name = albumNeuInput.value.trim();
            if (!name) return;
            var fd = new FormData();
            fd.append('action', 'alben_create');
            fd.append('name', name);
            fetch(API_MUSIK, { method: 'POST', body: fd })
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    if (data.ok) { albumNeuInput.value = ''; loadAlbenListe(); }
                    else if (statusEl) statusEl.textContent = data.error || 'Fehler';
                });
        });
        albumNeuInput.addEventListener('keydown', function(e) {
            if (e.key === 'Enter') albumErstellenBtn.click();
        });
    }

    // Alben-Liste (Bearbeiten / Löschen)
    var albenListe = document.getElementById('musikAlbenListe');
    if (albenListe) {
        albenListe.addEventListener('click', function(e) {
            var t = e.target;
            var editId = t && t.getAttribute('data-album-edit');
            var delId = t && t.getAttribute('data-album-delete');
            if (editId) {
                var overlay = document.getElementById('albumEditOverlay');
                var idInp = document.getElementById('albumEditId');
                var nameInp = document.getElementById('albumEditName');
                var preview = document.getElementById('albumEditPreview');
                var removeBtn = document.getElementById('albumEditIconRemove');
                var iconInput = document.getElementById('albumEditIconInput');
                if (overlay && idInp && nameInp && preview) {
                    idInp.value = editId;
                    nameInp.value = t.getAttribute('data-album-name') || '';
                    preview.src = '/album_cover.php?album_id=' + editId + '&t=' + Date.now();
                    preview.style.display = '';
                    if (removeBtn) removeBtn.style.display = (t.getAttribute('data-album-has-icon') === '1') ? '' : 'none';
                    if (iconInput) iconInput.value = '';
                    overlay.setAttribute('aria-hidden', 'false');
                }
                return;
            }
            if (delId) {
                if (!confirm('Playlist wirklich l\u00f6schen? Die Songs bleiben erhalten.')) return;
                var fd = new FormData();
                fd.append('action', 'alben_delete');
                fd.append('id', delId);
                fetch(API_MUSIK, { method: 'POST', body: fd })
                    .then(function(r) { return r.json(); })
                    .then(function(data) { if (data.ok) { loadAlbenListe(); loadMusikListe(); } });
            }
        });
    }

    // Album Edit Overlay
    var albumEditCancel = document.getElementById('albumEditCancel');
    var albumEditSave = document.getElementById('albumEditSave');
    var albumEditOverlay = document.getElementById('albumEditOverlay');
    var albumEditIconBtn = document.getElementById('albumEditIconBtn');
    var albumEditIconInput = document.getElementById('albumEditIconInput');
    var albumEditIconRemove = document.getElementById('albumEditIconRemove');
    if (albumEditCancel && albumEditOverlay) albumEditCancel.addEventListener('click', function() { albumEditOverlay.setAttribute('aria-hidden', 'true'); });
    if (albumEditIconBtn && albumEditIconInput) albumEditIconBtn.addEventListener('click', function() { albumEditIconInput.click(); });
    if (albumEditIconInput) albumEditIconInput.addEventListener('change', function() {
        var file = albumEditIconInput.files && albumEditIconInput.files[0];
        if (file && window.URL) {
            var prev = document.getElementById('albumEditPreview');
            if (prev) { prev.src = URL.createObjectURL(file); prev.style.display = ''; }
        }
    });
    if (albumEditIconRemove) albumEditIconRemove.addEventListener('click', function() {
        var idInp = document.getElementById('albumEditId');
        if (!idInp || !idInp.value) return;
        var fd = new FormData();
        fd.append('action', 'alben_icon_remove');
        fd.append('id', idInp.value);
        fetch(API_MUSIK, { method: 'POST', body: fd })
            .then(function(r) { return r.json(); })
            .then(function(data) {
                if (data.ok) {
                    var prev = document.getElementById('albumEditPreview');
                    if (prev) prev.src = '/album_cover.php?album_id=' + idInp.value + '&t=' + Date.now();
                    albumEditIconRemove.style.display = 'none';
                    loadAlbenListe();
                }
            });
    });
    if (albumEditSave && albumEditOverlay) albumEditSave.addEventListener('click', function() {
        var idInp = document.getElementById('albumEditId');
        var nameInp = document.getElementById('albumEditName');
        var iconInput = document.getElementById('albumEditIconInput');
        if (!idInp || !idInp.value) return;
        var fd = new FormData();
        fd.append('action', 'alben_update');
        fd.append('id', idInp.value);
        fd.append('name', nameInp ? nameInp.value.trim() : '');
        if (iconInput && iconInput.files && iconInput.files[0]) fd.append('icon', iconInput.files[0]);
        fetch(API_MUSIK, { method: 'POST', body: fd })
            .then(function(r) { return r.json(); })
            .then(function(data) {
                if (data.ok) {
                    albumEditOverlay.setAttribute('aria-hidden', 'true');
                    loadAlbenListe();
                    loadMusikListe();
                    if (iconInput) iconInput.value = '';
                }
            });
    });
    musikSwitchSubtab('lokal');
}

function musikUpdateVolumeIcon() {
    var icon = document.getElementById('playerVolumeIcon');
    var sleepIcon = document.getElementById('sleepPlayerVolumeIcon');
    var html = '';
    if (musikAudio.muted || musikAudio.volume === 0) {
        html =
            '<polygon points="11,5 6,9 2,9 2,15 6,15 11,19"/>' +
            '<line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>';
    } else if (musikAudio.volume < 0.5) {
        html =
            '<polygon points="11,5 6,9 2,9 2,15 6,15 11,19"/>' +
            '<path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>';
    } else {
        html =
            '<polygon points="11,5 6,9 2,9 2,15 6,15 11,19"/>' +
            '<path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>' +
            '<path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>';
    }
    if (icon) icon.innerHTML = html;
    if (sleepIcon) sleepIcon.innerHTML = html;
    var sleepSlider = document.getElementById('sleepPlayerVolumeSlider');
    if (sleepSlider) sleepSlider.value = String(Math.round((musikAudio.volume || 0) * 100));
}

// ── Equalizer (Web Audio API) ─────────────────────────────────────────────────
var musikAudioCtx    = null;
var musikAudioSource = null;
var musikEqFilters   = [];
var musikEqReady     = false;

var EQ_BANDS = [
    { freq: 60,    type: 'lowshelf'  },
    { freq: 250,   type: 'peaking'   },
    { freq: 1000,  type: 'peaking'   },
    { freq: 4000,  type: 'peaking'   },
    { freq: 16000, type: 'highshelf' }
];

function musikInitAudioCtx() {
    if (musikEqReady) return true;
    try {
        musikAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
        musikAudioSource = musikAudioCtx.createMediaElementSource(musikAudio);

        musikEqFilters = EQ_BANDS.map(function(b) {
            var f = musikAudioCtx.createBiquadFilter();
            f.type = b.type;
            f.frequency.value = b.freq;
            f.gain.value = 0;
            return f;
        });

        // Kette: source → filter0 → … → filter4 → destination
        musikAudioSource.connect(musikEqFilters[0]);
        for (var i = 0; i < musikEqFilters.length - 1; i++) {
            musikEqFilters[i].connect(musikEqFilters[i + 1]);
        }
        musikEqFilters[musikEqFilters.length - 1].connect(musikAudioCtx.destination);

        // Gespeicherte EQ-Werte laden
        var saved = localStorage.getItem('wecker-eq');
        if (saved) {
            try {
                var vals = JSON.parse(saved);
                vals.forEach(function(v, i) {
                    if (musikEqFilters[i]) musikEqFilters[i].gain.value = v;
                });
            } catch(e) {}
        }

        musikEqReady = true;
        return true;
    } catch(e) {
        console.warn('[EQ] AudioContext nicht verfügbar:', e);
        return false;
    }
}

function musikEqSetBand(index, db) {
    if (musikEqFilters[index]) musikEqFilters[index].gain.value = db;
    var dbEl = document.getElementById('eqDb' + index);
    if (dbEl) {
        dbEl.textContent = db > 0 ? '+' + db : String(db);
        dbEl.classList.toggle('zero', db === 0);
    }
}

function musikEqSave() {
    var vals = musikEqFilters.map(function(f) { return Math.round(f.gain.value); });
    localStorage.setItem('wecker-eq', JSON.stringify(vals));
}

function musikEqReset() {
    EQ_BANDS.forEach(function(_, i) {
        var slider = document.getElementById('eqSlider' + i);
        if (slider) slider.value = 0;
        musikEqSetBand(i, 0);
    });
    localStorage.removeItem('wecker-eq');
}

function musikEqUpdateResetVisibility() {
    var resetBtn = document.getElementById('eqResetBtn');
    if (!resetBtn) return;
    var anyNonZero = EQ_BANDS.some(function(_, i) {
        var s = document.getElementById('eqSlider' + i);
        return s && parseInt(s.value, 10) !== 0;
    });
    resetBtn.style.display = anyNonZero ? '' : 'none';
}

function initEqualizer() {
    var toggleBtn = document.getElementById('eqToggleBtn');
    var eqPanel   = document.getElementById('playerEq');
    var resetBtn  = document.getElementById('eqResetBtn');

    // Gespeicherten Offen-Status wiederherstellen
    if (localStorage.getItem('wecker-eq-open') === '1' && toggleBtn && eqPanel) {
        eqPanel.classList.add('open');
        toggleBtn.setAttribute('aria-expanded', 'true');
    }

    // Gespeicherte Slider-Werte ins UI laden (ohne AudioCtx – der wird erst bei Play init.)
    var saved = localStorage.getItem('wecker-eq');
    if (saved) {
        try {
            var vals = JSON.parse(saved);
            vals.forEach(function(v, i) {
                var slider = document.getElementById('eqSlider' + i);
                var dbEl   = document.getElementById('eqDb' + i);
                if (slider) slider.value = v;
                if (dbEl)   { dbEl.textContent = v > 0 ? '+' + v : String(v); dbEl.classList.toggle('zero', v === 0); }
            });
        } catch(e) {}
    } else {
        EQ_BANDS.forEach(function(_, i) {
            var dbEl = document.getElementById('eqDb' + i);
            if (dbEl) dbEl.classList.add('zero');
        });
    }
    musikEqUpdateResetVisibility();

    // Toggle Panel
    if (toggleBtn && eqPanel) {
        toggleBtn.addEventListener('click', function() {
            var open = eqPanel.classList.toggle('open');
            toggleBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
            eqPanel.setAttribute('aria-hidden', open ? 'false' : 'true');
            localStorage.setItem('wecker-eq-open', open ? '1' : '0');
        });
    }

    // Reset
    if (resetBtn) {
        resetBtn.addEventListener('click', function() {
            musikEqReset();
            musikEqUpdateResetVisibility();
        });
    }

    // Slider-Events
    EQ_BANDS.forEach(function(_, i) {
        var slider = document.getElementById('eqSlider' + i);
        if (!slider) return;
        slider.addEventListener('input', function() {
            var db = parseInt(slider.value, 10);
            // AudioCtx bei erster Interaktion initialisieren
            if (!musikEqReady) musikInitAudioCtx();
            if (musikAudioCtx && musikAudioCtx.state === 'suspended') musikAudioCtx.resume();
            musikEqSetBand(i, db);
            musikEqSave();
            musikEqUpdateResetVisibility();
        });
    });
}

// EQ-Initialisierung auch bei play() aufrufen (AudioCtx braucht User-Gesture)
var _origMusikPlayTrack = musikPlayTrack;
musikPlayTrack = function(index) {
    if (!musikEqReady) {
        musikInitAudioCtx();
        // Gespeicherte Werte in Filter schreiben, falls jetzt erst init
        var saved = localStorage.getItem('wecker-eq');
        if (saved && musikEqReady) {
            try {
                var vals = JSON.parse(saved);
                vals.forEach(function(v, i) { musikEqSetBand(i, v); });
            } catch(e) {}
        }
    }
    if (musikAudioCtx && musikAudioCtx.state === 'suspended') musikAudioCtx.resume();
    _origMusikPlayTrack(index);
};

// ── Statistiken ──────────────────────────────────────────────────────────────
var statsCurrentRange = '24h';
var statsAutoRefresh  = null;

function loadStatsPanel(range) {
    range = range || statsCurrentRange;
    statsCurrentRange = range;

    document.querySelectorAll('#statsRangeRow .stats-range-btn').forEach(function(btn) {
        btn.classList.toggle('active', btn.dataset.range === range);
    });

    var cpuWrap   = document.getElementById('statsCpuChartWrap');
    var powerWrap = document.getElementById('statsPowerChartWrap');
    if (cpuWrap)   cpuWrap.innerHTML   = '<div class="stats-loading">Lade…</div>';
    if (powerWrap) powerWrap.innerHTML = '<div class="stats-loading">Lade…</div>';

    var cpuIds   = ['statsCpuCurrent','statsCpuMin','statsCpuMax','statsCpuAvg'];
    var powerIds = ['statsPowerCurrent','statsPowerMin','statsPowerMax','statsPowerAvg'];

    fetch(API_STATS + '?action=history&range=' + encodeURIComponent(range))
        .then(function(r) { return r.json(); })
        .then(function(d) {
            if (!d.ok || !d.data || d.data.length < 2) {
                var msg = '<div class="stats-empty">Noch keine Daten – warte ca. 2 Minuten.</div>';
                if (cpuWrap)   cpuWrap.innerHTML   = msg;
                if (powerWrap) powerWrap.innerHTML = msg;
                cpuIds.concat(powerIds).forEach(function(id) {
                    var el = document.getElementById(id); if (el) { el.textContent = '–'; el.style.color = ''; }
                });
                return;
            }

            var data = d.data;

            // CPU-Temperatur KPIs
            var cpuVals = data.filter(function(p) { return p.cpu_temp !== null; }).map(function(p) { return p.cpu_temp; });
            if (cpuVals.length > 0) {
                var cpuLast = cpuVals[cpuVals.length - 1];
                var cpuMin  = Math.min.apply(null, cpuVals);
                var cpuMax  = Math.max.apply(null, cpuVals);
                var cpuAvg  = cpuVals.reduce(function(a, b) { return a + b; }, 0) / cpuVals.length;
                var cpuColor = cpuLast >= 80 ? '#ef4444' : cpuLast >= 70 ? '#f59e0b' : '';
                var setKpi = function(id, val, color) {
                    var el = document.getElementById(id);
                    if (!el) return;
                    el.textContent = val;
                    el.style.color = color || '';
                };
                setKpi('statsCpuCurrent', cpuLast.toFixed(1) + ' °C', cpuColor);
                setKpi('statsCpuMin',     cpuMin.toFixed(1)  + ' °C', '');
                setKpi('statsCpuMax',     cpuMax.toFixed(1)  + ' °C', cpuMax >= 80 ? '#ef4444' : cpuMax >= 70 ? '#f59e0b' : '');
                setKpi('statsCpuAvg',     cpuAvg.toFixed(1)  + ' °C', '');
            } else {
                cpuIds.forEach(function(id) { var el = document.getElementById(id); if (el) { el.textContent = '–'; el.style.color = ''; } });
            }
            statsDrawChart('statsCpuChartWrap', data, 'cpu_temp', 'CPU-Temperatur', '°C', 'var(--accent)', 70, 80, range);

            // Stromverbrauch KPIs
            var wVals = data.filter(function(p) { return p.watts !== null; }).map(function(p) { return p.watts; });
            if (wVals.length > 0) {
                var wLast = wVals[wVals.length - 1];
                var wMin  = Math.min.apply(null, wVals);
                var wMax  = Math.max.apply(null, wVals);
                var wAvg  = wVals.reduce(function(a, b) { return a + b; }, 0) / wVals.length;
                var setKpi2 = function(id, val) { var el = document.getElementById(id); if (el) { el.textContent = val; el.style.color = ''; } };
                setKpi2('statsPowerCurrent', wLast.toFixed(2) + ' W');
                setKpi2('statsPowerMin',     wMin.toFixed(2)  + ' W');
                setKpi2('statsPowerMax',     wMax.toFixed(2)  + ' W');
                setKpi2('statsPowerAvg',     wAvg.toFixed(2)  + ' W');
            } else {
                powerIds.forEach(function(id) { var el = document.getElementById(id); if (el) { el.textContent = '–'; el.style.color = ''; } });
            }
            statsDrawChart('statsPowerChartWrap', data, 'watts', 'Stromverbrauch', 'W', '#10b981', null, null, range);
        })
        .catch(function() {
            if (cpuWrap)   cpuWrap.innerHTML   = '<div class="stats-empty">Fehler beim Laden.</div>';
            if (powerWrap) powerWrap.innerHTML = '<div class="stats-empty">Fehler beim Laden.</div>';
        });

    // ── Raumklima (SCD41) ───────────────────────────────────────────────────
    var co2Wrap  = document.getElementById('statsCo2ChartWrap');
    var tempWrap = document.getElementById('statsTempChartWrap');
    var humWrap  = document.getElementById('statsHumChartWrap');
    var sensorIds = ['statsCo2Current','statsCo2Min','statsCo2Max','statsCo2Avg',
                     'statsTempCurrent','statsTempMin','statsTempMax','statsTempAvg',
                     'statsHumCurrent','statsHumMin','statsHumMax','statsHumAvg'];
    if (co2Wrap)  co2Wrap.innerHTML  = '<div class="stats-loading">Lade…</div>';
    if (tempWrap) tempWrap.innerHTML = '<div class="stats-loading">Lade…</div>';
    if (humWrap)  humWrap.innerHTML  = '<div class="stats-loading">Lade…</div>';

    fetch(API_STATS + '?action=sensors&range=' + encodeURIComponent(range))
        .then(function(r) { return r.json(); })
        .then(function(d) {
            if (!d.ok || !d.data || d.data.length < 1) {
                var msg = '<div class="stats-empty">Noch keine Sensordaten – warte ca. 1 Minute.</div>';
                if (co2Wrap)  co2Wrap.innerHTML  = msg;
                if (tempWrap) tempWrap.innerHTML = msg;
                if (humWrap)  humWrap.innerHTML  = msg;
                sensorIds.forEach(function(id) {
                    var el = document.getElementById(id); if (el) { el.textContent = '–'; el.style.color = ''; }
                });
                return;
            }

            var sd = d.data;
            var setKpiS = function(id, val, color) {
                var el = document.getElementById(id);
                if (!el) return;
                el.textContent = val;
                el.style.color = color || '';
            };

            // CO2-KPIs (Grenzwerte: 800 ppm = gelb, 1200 ppm = rot)
            var co2Vals = sd.filter(function(p) { return p.co2 !== null; }).map(function(p) { return p.co2; });
            if (co2Vals.length > 0) {
                var co2Last = co2Vals[co2Vals.length - 1];
                var co2Color = co2Last >= 1200 ? '#ef4444' : co2Last >= 800 ? '#f59e0b' : '#10b981';
                setKpiS('statsCo2Current', co2Last + ' ppm', co2Color);
                setKpiS('statsCo2Min',     Math.min.apply(null, co2Vals) + ' ppm', '');
                var co2Max = Math.max.apply(null, co2Vals);
                setKpiS('statsCo2Max',     co2Max + ' ppm', co2Max >= 1200 ? '#ef4444' : co2Max >= 800 ? '#f59e0b' : '');
                setKpiS('statsCo2Avg',     Math.round(co2Vals.reduce(function(a,b){return a+b;},0)/co2Vals.length) + ' ppm', '');
            } else {
                ['statsCo2Current','statsCo2Min','statsCo2Max','statsCo2Avg'].forEach(function(id){
                    var el=document.getElementById(id); if(el){el.textContent='–';el.style.color='';}
                });
            }
            statsDrawChart('statsCo2ChartWrap', sd, 'co2', 'CO₂', 'ppm', '#f59e0b', 800, 1200, range);

            // Temperatur-KPIs
            var tempVals = sd.filter(function(p) { return p.temp_scd !== null; }).map(function(p) { return p.temp_scd; });
            if (tempVals.length > 0) {
                var tLast = tempVals[tempVals.length - 1];
                setKpiS('statsTempCurrent', tLast.toFixed(1) + ' °C', '');
                setKpiS('statsTempMin',     Math.min.apply(null, tempVals).toFixed(1) + ' °C', '');
                setKpiS('statsTempMax',     Math.max.apply(null, tempVals).toFixed(1) + ' °C', '');
                setKpiS('statsTempAvg',     (tempVals.reduce(function(a,b){return a+b;},0)/tempVals.length).toFixed(1) + ' °C', '');
            } else {
                ['statsTempCurrent','statsTempMin','statsTempMax','statsTempAvg'].forEach(function(id){
                    var el=document.getElementById(id); if(el){el.textContent='–';el.style.color='';}
                });
            }
            statsDrawChart('statsTempChartWrap', sd, 'temp_scd', 'Temperatur', '°C', '#f97316', null, null, range);

            // Luftfeuchtigkeits-KPIs
            var humVals = sd.filter(function(p) { return p.hum_scd !== null; }).map(function(p) { return p.hum_scd; });
            if (humVals.length > 0) {
                var hLast = humVals[humVals.length - 1];
                setKpiS('statsHumCurrent', hLast.toFixed(1) + ' %', '');
                setKpiS('statsHumMin',     Math.min.apply(null, humVals).toFixed(1) + ' %', '');
                setKpiS('statsHumMax',     Math.max.apply(null, humVals).toFixed(1) + ' %', '');
                setKpiS('statsHumAvg',     (humVals.reduce(function(a,b){return a+b;},0)/humVals.length).toFixed(1) + ' %', '');
            } else {
                ['statsHumCurrent','statsHumMin','statsHumMax','statsHumAvg'].forEach(function(id){
                    var el=document.getElementById(id); if(el){el.textContent='–';el.style.color='';}
                });
            }
            statsDrawChart('statsHumChartWrap', sd, 'hum_scd', 'Luftfeuchtigkeit', '%', '#38bdf8', null, null, range);
        })
        .catch(function() {
            var msg = '<div class="stats-empty">Fehler beim Laden.</div>';
            if (co2Wrap)  co2Wrap.innerHTML  = msg;
            if (tempWrap) tempWrap.innerHTML = msg;
            if (humWrap)  humWrap.innerHTML  = msg;
        });
}

function statsDrawChart(wrapId, data, key, label, unit, strokeColor, warnLevel, dangerLevel, range) {
    var wrap = document.getElementById(wrapId);
    if (!wrap) return;

    var pts = data.filter(function(d) { return d[key] !== null; }).map(function(d) {
        return { t: new Date(d.t.replace(' ', 'T')).getTime(), v: +d[key] };
    });

    if (pts.length < 2) {
        wrap.innerHTML = '<div class="stats-empty">Nicht genug Daten (min. 2 Messpunkte).</div>';
        return;
    }

    var W = 560, H = 160, pl = 40, pr = 12, pt = 16, pb = 28;
    var cw = W - pl - pr, ch = H - pt - pb;
    var minT = pts[0].t, maxT = pts[pts.length - 1].t;
    var vals = pts.map(function(p) { return p.v; });
    var minV = Math.min.apply(null, vals), maxV = Math.max.apply(null, vals);
    var vr = maxV - minV;

    if (key === 'cpu_temp') {
        if (vr < 5)  { var mid = (minV + maxV) / 2; minV = Math.max(0, mid - 3); maxV = mid + 3; }
        var pad = (maxV - minV) * 0.12;
        minV = Math.max(0, minV - pad);
        maxV = maxV + pad;
    } else {
        if (vr < 0.1) { var mid2 = (minV + maxV) / 2; minV = Math.max(0, mid2 - 0.5); maxV = mid2 + 0.5; }
        var pad2 = (maxV - minV) * 0.12;
        minV = Math.max(0, minV - pad2);
        maxV = maxV + pad2;
    }

    function tx(t) { return pl + ((t - minT) / (maxT - minT || 1)) * cw; }
    function ty(v)  { return pt + ch - ((v - minV) / (maxV - minV || 1)) * ch; }
    function fv(v)  { return v >= 100 ? v.toFixed(0) : v >= 10 ? v.toFixed(1) : v.toFixed(2); }

    var gradId = 'sg_' + wrapId;
    var svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + W + ' ' + H + '" width="100%" role="img" aria-label="' + label + ' Verlauf">';
    svg += '<defs>';
    svg += '<linearGradient id="' + gradId + '" x1="0" y1="0" x2="0" y2="1">';
    svg += '<stop offset="0%" stop-color="' + strokeColor + '" stop-opacity="0.25"/>';
    svg += '<stop offset="100%" stop-color="' + strokeColor + '" stop-opacity="0.02"/>';
    svg += '</linearGradient>';
    svg += '</defs>';

    // Gefahren-Zonen (CPU-Temperatur)
    if (dangerLevel !== null && dangerLevel <= maxV) {
        var dzy = ty(dangerLevel);
        svg += '<rect x="' + pl + '" y="' + pt + '" width="' + cw + '" height="' + Math.max(0, dzy - pt).toFixed(1) + '" fill="#ef444418" rx="0"/>';
    }
    if (warnLevel !== null && warnLevel <= maxV) {
        var wzy = ty(warnLevel);
        var wzy2 = (dangerLevel !== null && dangerLevel <= maxV) ? ty(dangerLevel) : (pt + ch);
        svg += '<rect x="' + pl + '" y="' + wzy.toFixed(1) + '" width="' + cw + '" height="' + Math.max(0, wzy2 - wzy).toFixed(1) + '" fill="#f59e0b18" rx="0"/>';
    }

    // Horizontale Gitternetzlinien (4 Intervalle)
    for (var i = 0; i <= 4; i++) {
        var gy = (pt + (ch / 4) * i).toFixed(1);
        var gv = maxV - (maxV - minV) / 4 * i;
        svg += '<line x1="' + pl + '" y1="' + gy + '" x2="' + (W - pr) + '" y2="' + gy + '" stroke="currentColor" stroke-opacity="0.1" stroke-width="1"/>';
        svg += '<text x="' + (pl - 4) + '" y="' + (parseFloat(gy) + 4).toFixed(1) + '" text-anchor="end" fill="currentColor" fill-opacity="0.5" font-size="9" font-family="system-ui,sans-serif">' + fv(gv) + '</text>';
    }

    // Füll-Fläche
    var area = 'M' + tx(pts[0].t).toFixed(1) + ',' + (pt + ch);
    pts.forEach(function(p) { area += ' L' + tx(p.t).toFixed(1) + ',' + ty(p.v).toFixed(1); });
    area += ' L' + tx(pts[pts.length - 1].t).toFixed(1) + ',' + (pt + ch) + ' Z';
    svg += '<path d="' + area + '" fill="url(#' + gradId + ')"/>';

    // Linie
    var line = pts.map(function(p) { return tx(p.t).toFixed(1) + ',' + ty(p.v).toFixed(1); }).join(' ');
    svg += '<polyline points="' + line + '" fill="none" stroke="' + strokeColor + '" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/>';

    // Letzter Punkt hervorheben
    var lp = pts[pts.length - 1];
    svg += '<circle cx="' + tx(lp.t).toFixed(1) + '" cy="' + ty(lp.v).toFixed(1) + '" r="3" fill="' + strokeColor + '"/>';

    // X-Achsen-Beschriftungen (4 Punkte)
    var is7d = (range === '7d');
    var days = ['So','Mo','Di','Mi','Do','Fr','Sa'];
    for (var j = 0; j <= 4; j++) {
        var lt  = minT + (maxT - minT) / 4 * j;
        var lx  = (pl + cw / 4 * j).toFixed(1);
        var ld  = new Date(lt);
        var xlbl = is7d
            ? (days[ld.getDay()] + ' ' + ld.getHours().toString().padStart(2, '0') + ':00')
            : (ld.getHours().toString().padStart(2, '0') + ':' + ld.getMinutes().toString().padStart(2, '0'));
        svg += '<text x="' + lx + '" y="' + (H - 6) + '" text-anchor="middle" fill="currentColor" fill-opacity="0.5" font-size="9" font-family="system-ui,sans-serif">' + xlbl + '</text>';
    }

    svg += '</svg>';
    wrap.innerHTML = svg;
}

var VALID_PANELS = ['panelAlarm', 'panelTimer', 'panelWetter', 'panelMusik'];
function showPanel(panelId) {
    if (VALID_PANELS.indexOf(panelId) === -1) return;
    document.querySelectorAll('.panel').forEach(function(p) { p.classList.remove('active'); });
    document.querySelectorAll('.bottom-nav button[data-panel]').forEach(function(b) { b.classList.remove('active'); });
    var p = document.getElementById(panelId);
    var btn = document.querySelector('.bottom-nav button[data-panel="' + panelId + '"]');
    if (p) p.classList.add('active');
    if (btn) btn.classList.add('active');
    localStorage.setItem('wecker-active-tab', panelId);
    if (panelId === 'panelWetter') loadWetterFromStorage();
    if (panelId === 'panelMusik') { loadAlbenListe(); loadMusikListe(); }
}

function applyAccentColor(hex) {
    var root = document.documentElement;
    if (!hex || hex === '') {
        root.style.removeProperty('--accent');
        try { localStorage.removeItem('wecker-accent'); } catch (e) {}
        return;
    }
    root.style.setProperty('--accent', hex);
    try { localStorage.setItem('wecker-accent', hex); } catch (e) {}
}

document.addEventListener('DOMContentLoaded', function() {
    var accent = null;
    try { accent = localStorage.getItem('wecker-accent'); } catch (e) {}
    if (accent) applyAccentColor(accent);

    var loading = document.getElementById('loadingScreen');
    if (loading) {
        loading.classList.add('hidden');
        setTimeout(function() { loading.remove(); }, 400);
    }
    initZeitzoneDropdown();
    var btnTheme = document.getElementById('themeToggle');
    if (btnTheme) btnTheme.addEventListener('click', toggleTheme);
    ['themeToggle4','themeToggle5','themeToggleMusik'].forEach(function(id) {
        var b = document.getElementById(id);
        if (b) b.addEventListener('click', toggleTheme);
    });
    var btnNeu = document.getElementById('btnNeuWecker');
    if (btnNeu) btnNeu.addEventListener('click', neuWecker);
    document.querySelectorAll('.btn-open-settings').forEach(function(btn) {
        btn.addEventListener('click', openSettings);
    });

    document.querySelectorAll('.settings-tabs button[data-tab]').forEach(function(btn) {
        btn.addEventListener('click', function() {
            showSettingsTab(btn.getAttribute('data-tab'));
            if (btn.getAttribute('data-tab') === 'settingsTabInfo')  loadInfoTab();
            if (btn.getAttribute('data-tab') === 'settingsTabDebug') loadDebugTab();
            if (btn.getAttribute('data-tab') === 'settingsTabUpdate') loadUpdateTab();
            if (btn.getAttribute('data-tab') === 'settingsTabWlan') wlanLoadStatus();
            if (btn.getAttribute('data-tab') === 'settingsTabStatistik') loadStatsPanel(statsCurrentRange);
        });
    });

    document.querySelectorAll('#statsRangeRow .stats-range-btn').forEach(function(btn) {
        btn.addEventListener('click', function() { loadStatsPanel(btn.dataset.range); });
    });

    var btnCronRunOnce = document.getElementById('btnCronRunOnce');
    if (btnCronRunOnce) {
        btnCronRunOnce.addEventListener('click', function() {
            var btn = this;
            var origText = btn.textContent;
            btn.disabled = true;
            btn.textContent = 'Läuft…';
            fetch(API_INFO + '?action=run').then(function(r) { return r.json(); }).then(function(data) {
                btn.disabled = false;
                btn.textContent = origText;
                if (data && data.ok) {
                    var msg = document.getElementById('settingsMsg');
                    if (msg) { msg.textContent = data.message || 'Cron einmal ausgeführt.'; msg.className = 'settings-msg'; }
                    loadDebugTab();
                } else {
                    var msg = document.getElementById('settingsMsg');
                    if (msg) { msg.textContent = data && data.message ? data.message : 'Cron-Ausführung fehlgeschlagen.'; msg.className = 'settings-msg error'; }
                }
            }).catch(function() {
                btn.disabled = false;
                btn.textContent = origText;
                var msg = document.getElementById('settingsMsg');
                if (msg) { msg.textContent = 'Cron-Ausführung fehlgeschlagen.'; msg.className = 'settings-msg error'; }
            });
        });
    }

    var btnResetCache = document.getElementById('btnResetCache');
    if (btnResetCache) {
        btnResetCache.addEventListener('click', function() {
            var btn = this;
            var origText = btn.textContent;
            btn.disabled = true;
            btn.textContent = 'Wird geleert…';
            fetch(API_INFO + '?action=reset_cache').then(function(r) { return r.json(); }).then(function(data) {
                btn.disabled = false;
                btn.textContent = origText;
                var msg = document.getElementById('settingsMsg');
                if (msg) {
                    msg.textContent = (data && data.ok) ? (data.message || 'Cache wurde zurückgesetzt.') : (data && data.message ? data.message : 'Cache zurücksetzen fehlgeschlagen.');
                    msg.className = 'settings-msg' + (data && data.ok ? '' : ' error');
                }
            }).catch(function() {
                btn.disabled = false;
                btn.textContent = origText;
                var msg = document.getElementById('settingsMsg');
                if (msg) { msg.textContent = 'Cache zurücksetzen fehlgeschlagen.'; msg.className = 'settings-msg error'; }
            });
        });
    }

    var btnCleanupScan = document.getElementById('btnCleanupScan');
    if (btnCleanupScan) {
        btnCleanupScan.addEventListener('click', function() { loadCleanupScan(); });
    }

    var btnReboot = document.getElementById('btnReboot');
    if (btnReboot) {
        btnReboot.addEventListener('click', function() { systemCommand('reboot'); });
    }
    var btnShutdown = document.getElementById('btnShutdown');
    if (btnShutdown) {
        btnShutdown.addEventListener('click', function() { systemCommand('shutdown'); });
    }
    var btnKillFirefox = document.getElementById('btnKillFirefox');
    if (btnKillFirefox) {
        btnKillFirefox.addEventListener('click', killFirefox);
    }

    var alarmSnoozeBtn = document.getElementById('alarmSnoozeBtn');
    if (alarmSnoozeBtn) alarmSnoozeBtn.addEventListener('click', alarmSnooze);
    var alarmStopBtn = document.getElementById('alarmStopBtn');
    if (alarmStopBtn) alarmStopBtn.addEventListener('click', alarmStop);

    document.querySelectorAll('.bottom-nav button[data-panel]').forEach(function(btn) {
        btn.addEventListener('click', function() { showPanel(btn.getAttribute('data-panel')); });
    });

    var savedTab = localStorage.getItem('wecker-active-tab');
    if (savedTab && VALID_PANELS.indexOf(savedTab) !== -1 && document.getElementById(savedTab)) {
        showPanel(savedTab);
    }

    initTimer();
    initWetter();
    initMusik();
    initEqualizer();
    initUpdateTab();
    initSleepMode();
    initPlatzhalterDragDrop();
    initSprachtextToggle();
    initAudioSearch();
    initTouchKeyboard();
    initWochentageToggle();

    setTimeout(function() { checkForUpdateNotification(); }, 1500);

    var updateToastToSettings = document.getElementById('updateToastToSettings');
    var updateToastClose = document.getElementById('updateToastClose');
    if (updateToastToSettings) {
        updateToastToSettings.addEventListener('click', function() {
            hideUpdateToast();
            openSettings();
            showSettingsTab('settingsTabUpdate');
            loadUpdateTab();
        });
    }
    if (updateToastClose) updateToastClose.addEventListener('click', hideUpdateToast);
});

function checkForUpdateNotification() {
    var cached = updateCacheGet();
    if (cached) {
        _evalUpdateNotification(cached.current, cached.list);
        return;
    }
    Promise.all([
        fetch(API_UPDATE + '?action=current').then(function(r) { return r.json(); }),
        fetch(API_UPDATE + '?action=list').then(function(r) { return r.json(); })
    ]).then(function(results) {
        var current = results[0];
        var list = results[1];
        updateCacheSet(current, list);
        _evalUpdateNotification(current, list);
    }).catch(function() {});
}
function _evalUpdateNotification(current, list) {
    if (!list || !list.ok || !list.versionen || list.versionen.length === 0) return;
    var latest = list.versionen[0];
    var latestVer = latest && (latest.version || '');
    var currentVer = (current && current.ok && current.version != null) ? String(current.version) : '0.0.0';
    if (latestVer && latestVer !== currentVer) {
        showUpdateToast();
    }
}

function showUpdateToast() {
    var bar = document.getElementById('updateBar');
    if (bar) bar.classList.add('visible');
}

function hideUpdateToast() {
    var bar = document.getElementById('updateBar');
    if (bar) bar.classList.remove('visible');
}

var audioSearchSongs = [];
var audioSearchPlaylists = [];
var audioSearchRadios = [];
var RADIO_STATIONS = [
    { label: 'Antenne Bayern', stream_url: 'https://s2-webradio.antenne.de/antenne' },
    { label: 'ROCK ANTENNE', stream_url: 'https://s6-webradio.rockantenne.de/rockantenne' },
    { label: 'Bayern 3', stream_url: 'https://dispatcher.rndfnk.com/br/br3/live/aac/low' },
    { label: 'Bayern 1', stream_url: 'https://dispatcher.rndfnk.com/br/br1/live/mp3/low' },
    { label: 'Radio Gong 96.3', stream_url: 'https://stream.radiogong.de/radiogong/stream/mp3' }
];

function radioLabelByDatei(datei) {
    var value = String(datei || '');
    if (value.indexOf('radio:') !== 0) return '';
    var streamUrl = value.slice(6);
    var hit = RADIO_STATIONS.find(function(station) {
        return String(station.stream_url) === streamUrl;
    });
    return hit ? hit.label : 'Radio-Stream';
}

function loadAudioSearchData(cb) {
    if (window.audioSearchDataLoaded) { if (cb) cb(); return; }
    Promise.all([
        fetch(API_MUSIK + '?action=list').then(function(r) { return r.json(); }),
        fetch(API_MUSIK + '?action=alben_list').then(function(r) { return r.json(); })
    ]).then(function(results) {
        var listData = results[0].ok && results[0].files ? results[0].files : [];
        var albenData = results[1].ok && results[1].alben ? results[1].alben : [];
        audioSearchSongs = listData.map(function(f) {
            var label = (f.titel && f.titel.trim()) ? f.titel.trim() : f.name;
            return { type: 'song', id: f.name, label: label };
        });
        audioSearchPlaylists = albenData.map(function(a) {
            return { type: 'playlist', id: a.id, label: a.name };
        });
        audioSearchRadios = RADIO_STATIONS.map(function(station) {
            return { type: 'radio', id: station.stream_url, label: station.label };
        });
        window.audioSearchDataLoaded = true;
        if (cb) cb();
    }).catch(function() { if (cb) cb(); });
}

function filterAudioSearch(query, position) {
    var q = (query || '').trim().toLowerCase();
    if (!q) return [];
    var out = [];
    audioSearchPlaylists.forEach(function(p) {
        if (p.label.toLowerCase().indexOf(q) !== -1) out.push(p);
    });
    audioSearchRadios.forEach(function(r) {
        if (r.label.toLowerCase().indexOf(q) !== -1) out.push(r);
    });
    audioSearchSongs.forEach(function(s) {
        if (s.label.toLowerCase().indexOf(q) !== -1) out.push(s);
    });
    return out.slice(0, 15);
}

function showAudioSearchResults(inputEl, position) {
    var query = (inputEl.value || '').trim();
    var resultsUl = document.getElementById('audioSearchResults' + (position === 'gleichzeitig' ? 'Gleichzeitig' : 'Danach'));
    if (!resultsUl) return;
    var results = filterAudioSearch(query, position);
    resultsUl.innerHTML = '';
    resultsUl.setAttribute('aria-hidden', 'true');
    if (results.length === 0) { resultsUl.style.display = 'none'; return; }
    results.forEach(function(item) {
        var li = document.createElement('li');
        var typeLabel = item.type === 'playlist' ? 'Playlist' : (item.type === 'radio' ? 'Radio' : 'Lied');
        li.innerHTML = '<span class="audio-search-type">' + typeLabel + '</span> ' + escapeHtml(item.label);
        li.addEventListener('click', function() {
            addAudioFromSearch(position, item);
            inputEl.value = '';
            resultsUl.innerHTML = '';
            resultsUl.setAttribute('aria-hidden', 'true');
            resultsUl.style.display = 'none';
        });
        resultsUl.appendChild(li);
    });
    resultsUl.setAttribute('aria-hidden', 'false');
    resultsUl.style.display = 'block';
}

function addAudioFromSearch(position, item) {
    var datei = item.type === 'playlist' ? ('playlist:' + item.id) : (item.type === 'radio' ? ('radio:' + item.id) : item.id);
    var titel = item.label || (item.type === 'playlist' ? ('Playlist ' + item.id) : (item.type === 'radio' ? 'Radio-Stream' : item.id));
    audioList.push({ datei: datei, titel: titel, position: position });
    renderAudioLists();
}

function renderAudioLists() {
    ['gleichzeitig', 'danach'].forEach(function(pos) {
        var ul = document.getElementById('audioList' + pos.charAt(0).toUpperCase() + pos.slice(1));
        if (!ul) return;
        ul.innerHTML = '';
        audioList.filter(function(a) { return a.position === pos; }).forEach(function(a, i) {
            var idx = audioList.indexOf(a);
            var li = document.createElement('li');
            var label = (a.titel && a.titel.trim())
                ? a.titel
                : (a.datei.indexOf('playlist:') === 0
                    ? ('Playlist ' + a.datei.slice(9))
                    : (a.datei.indexOf('radio:') === 0 ? radioLabelByDatei(a.datei) : a.datei));
            li.innerHTML = '<span title="' + escapeHtml(a.datei) + '">' + escapeHtml(label) + '</span><button type="button" onclick="removeAudio(' + idx + ')">Entfernen</button>';
            ul.appendChild(li);
        });
    });
    var editAudioEl = document.getElementById('editAudio');
    if (editAudioEl) editAudioEl.value = JSON.stringify(audioList);
}

function initAudioSearch() {
    ['gleichzeitig', 'danach'].forEach(function(pos) {
        var inputId = 'audioSearch' + (pos === 'gleichzeitig' ? 'Gleichzeitig' : 'Danach');
        var input = document.getElementById(inputId);
        var resultsId = 'audioSearchResults' + (pos === 'gleichzeitig' ? 'Gleichzeitig' : 'Danach');
        if (!input) return;
        input.addEventListener('focus', function() {
            loadAudioSearchData(function() {
                showAudioSearchResults(input, pos);
            });
        });
        input.addEventListener('input', function() {
            loadAudioSearchData(function() { showAudioSearchResults(input, pos); });
        });
        input.addEventListener('blur', function() {
            setTimeout(function() {
                var ul = document.getElementById(resultsId);
                if (ul) { ul.innerHTML = ''; ul.setAttribute('aria-hidden', 'true'); ul.style.display = 'none'; }
            }, 200);
        });
    });
    document.addEventListener('click', function(e) {
        if (e.target.classList && e.target.classList.contains('audio-search-input')) return;
        if (e.target.closest && e.target.closest('.audio-search-results')) return;
        ['Gleichzeitig', 'Danach'].forEach(function(suffix) {
            var ul = document.getElementById('audioSearchResults' + suffix);
            if (ul) { ul.innerHTML = ''; ul.setAttribute('aria-hidden', 'true'); ul.style.display = 'none'; }
        });
    });
}

function removeAudio(index) {
    audioList.splice(index, 1);
    renderAudioLists();
}

function getNextAlarm() {
    const now = new Date();
    let next = null;
    const tag = now.getDay() || 7;
    const heute = now.toISOString().slice(0, 10);
    weckerListeData.forEach(function(w) {
        if (!w.aktiv) return;
        const [h, m] = (w.weckzeit || '00:00').split(':').map(Number);
        if (w.wiederholung === 'einmal') {
            if (!w.weckdatum) return;
            const d = new Date(w.weckdatum + 'T' + (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m + ':00');
            if (d > now && (!next || d < next.dateTime)) next = { dateTime: d, label: w.sprachtext || w.weckzeit };
        } else if (w.wiederholung === 'woechentlich' && w.wochentage && w.wochentage.length) {
            for (let i = 0; i <= 7; i++) {
                const d = new Date(now);
                d.setDate(d.getDate() + i);
                const t = d.getDay() || 7;
                if (w.wochentage.indexOf(t) === -1) continue;
                d.setHours(h, m, 0, 0);
                if (d > now && (!next || d < next.dateTime)) next = { dateTime: d, label: w.sprachtext || w.weckzeit };
            }
        } else {
            const d = new Date(now);
            d.setHours(h, m, 0, 0);
            if (d <= now) d.setDate(d.getDate() + 1);
            if (!next || d < next.dateTime) next = { dateTime: d, label: w.sprachtext || w.weckzeit };
        }
    });
    return next;
}

function getNextAlarms(limit) {
    const now = new Date();
    const candidates = [];
    weckerListeData.forEach(function(w) {
        if (!w.aktiv) return;
        const [h, m] = (w.weckzeit || '00:00').split(':').map(Number);
        const label = w.sprachtext || (w.weckzeit || '00:00').substring(0, 5);
        const meta = getMeta(w);
        const weckzeitStr = (w.weckzeit || '00:00').substring(0, 5);
        const payload = { dateTime: null, label: label, meta: meta, id: w.id, sprachtext: w.sprachtext || '', weckzeit: weckzeitStr };
        if (w.wiederholung === 'einmal' && w.weckdatum) {
            const d = new Date(w.weckdatum + 'T' + (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m + ':00');
            if (d > now) { payload.dateTime = d; candidates.push(payload); }
        } else if (w.wiederholung === 'woechentlich' && w.wochentage && w.wochentage.length) {
            for (let i = 0; i <= 7; i++) {
                const d = new Date(now);
                d.setDate(d.getDate() + i);
                const t = d.getDay() || 7;
                if (w.wochentage.indexOf(t) === -1) continue;
                d.setHours(h, m, 0, 0);
                if (d > now) {
                    candidates.push({ dateTime: new Date(d), label: label, meta: meta, id: w.id, sprachtext: w.sprachtext || '', weckzeit: weckzeitStr });
                    break;
                }
            }
        } else {
            const d = new Date(now);
            d.setHours(h, m, 0, 0);
            if (d <= now) d.setDate(d.getDate() + 1);
            payload.dateTime = d;
            candidates.push(payload);
        }
    });
    candidates.sort(function(a, b) { return a.dateTime - b.dateTime; });
    var seen = {};
    return candidates.filter(function(a) {
        var key = a.dateTime.getTime();
        if (seen[key]) return false;
        seen[key] = true;
        return true;
    }).slice(0, limit || 5);
}

function updateHeader() {
    const next = getNextAlarm();
    const titleEl = document.getElementById('headerTitle');
    const subEl = document.getElementById('headerSubtitle');
    if (!next) {
        titleEl.textContent = 'Kein Alarm';
        subEl.textContent = '–';
        return;
    }
    const min = Math.round((next.dateTime - new Date()) / 60000);
    if (min < 0) { updateHeader(); return; }
    if (min < 60) titleEl.textContent = 'Alarm in ' + min + ' Minute' + (min !== 1 ? 'n' : '');
    else if (min < 1440) titleEl.textContent = 'Alarm in ' + Math.round(min / 60) + ' Stunde' + (Math.round(min / 60) !== 1 ? 'n' : '');
    else titleEl.textContent = 'Alarm in ' + Math.round(min / 1440) + ' Tag' + (Math.round(min / 1440) !== 1 ? 'en' : '');
    const opts = { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' };
    subEl.textContent = next.dateTime.toLocaleDateString('de-DE', opts);
}
updateHeader();
setInterval(updateHeader, 60000);

function getMeta(w) {
    if (w.wiederholung === 'woechentlich' && w.wochentage && w.wochentage.length) {
        return w.wochentage.map(function(t) { return WOChentAGE_LABELS[t] || ''; }).join(' ');
    }
    if (w.wiederholung === 'einmal' && w.weckdatum) {
        var d = new Date(w.weckdatum + 'T12:00:00');
        if (!isNaN(d.getTime())) return d.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric', month: 'long' });
        return '';
    }
    return 'Täglich';
}

function renderWeckerListe(list) {
    var el = document.getElementById('weckerListe');
    if (!el) return;
    var html = '';
    (list || []).forEach(function(w) {
        var meta = getMeta(w);
        var label = (w.sprachtext || '').length > 25 ? (w.sprachtext || '').substring(0, 22) + '…' : (w.sprachtext || '');
        var aktiv = !!w.aktiv;
        html += '<div class="wecker ' + (aktiv ? '' : 'inaktiv') + '" data-id="' + w.id + '" onclick="editWecker(' + w.id + ')">';
        html += '<div class="wecker-left">';
        if (label) html += '<span class="wecker-label">' + escapeHtml(label) + '</span>';
        html += '<div class="weckzeit">' + escapeHtml((w.weckzeit || '00:00').substring(0, 5)) + '</div>';
        html += '<div class="wecker-meta">' + escapeHtml(meta) + '</div></div>';
        html += '<label class="toggle" onclick="event.stopPropagation()">';
        html += '<input type="checkbox" ' + (aktiv ? 'checked' : '') + ' onchange="toggleWecker(' + w.id + ', this.checked)"><span></span></label></div>';
    });
    el.innerHTML = html;
}

function refreshWeckerData() {
    var editOverlay = document.getElementById('editOverlay');
    if (editOverlay && editOverlay.style.display === 'flex') return;
    fetch(API_WECKER + '?action=list').then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok) return;
        var newHash = JSON.stringify(data.wecker || []);
        if (newHash === _weckerLiveHash) return;
        _weckerLiveHash = newHash;
        weckerListeData = data.wecker || [];
        renderWeckerListe(weckerListeData);
        updateHeader();
    }).catch(function() {});
}
setInterval(refreshWeckerData, 1000);

// ── Alarm-Klingel-Overlay ─────────────────────────────────────────────────────
// ─── Browser-Audio-Queue ────────────────────────────────────────────────────
var _alarmAudioQueue      = [];
var _alarmCurrentAudio    = null;
var _alarmAudioPlaying    = false;
var _alarmAutoplayBlocked = false;
var _alarmPendingUrls     = [];
var _alarmFallbackContext = null;
var _alarmFallbackTimer   = null;
var _alarmLocalCurrent    = null;
var _alarmAutoplayPrimed  = false;
var _alarmAutoplayPriming = null;
var _alarmSilentMedia     = null;
var ALARM_LOCAL_HANDLED_KEY = 'wecker-local-handled';
var ALARM_LOCAL_SNOOZE_KEY  = 'wecker-local-snooze';
var ALARM_SNOOZE_LABEL_HTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M12 6v6l3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/></svg> Snooze (5 Min)';

function alarmShowPlayBtn(show) {
    var btn = document.getElementById('alarmPlayAudioBtn');
    if (btn) btn.style.display = show ? '' : 'none';
}

function alarmAudioStart(urls) {
    alarmAudioStop();
    if (!urls || urls.length === 0) {
        alarmFallbackStart();
        return;
    }
    _alarmPendingUrls  = urls.slice();
    _alarmAudioQueue   = urls.slice();
    _alarmAudioPlaying = true;
    _alarmAutoplayBlocked = false;
    alarmShowPlayBtn(false);
    alarmAudioPlayNext();
}

function alarmAudioPlayNext() {
    if (!_alarmAudioPlaying || _alarmAudioQueue.length === 0) {
        _alarmAudioPlaying = false;
        _alarmCurrentAudio = null;
        return;
    }
    var url   = _alarmAudioQueue.shift();
    var audio = new Audio(url);
    _alarmCurrentAudio = audio;
    audio.onended = function() {
        if (_alarmAudioPlaying) alarmAudioPlayNext();
    };
    audio.onerror = function() {
        console.warn('[alarm] Audio-Fehler (404?):', url);
        if (_alarmAudioPlaying) alarmAudioPlayNext();
    };
    audio.play().catch(function(e) {
        // Firefox/Browser blockiert Autoplay ohne Nutzer-Interaktion
        if (e.name === 'NotAllowedError') {
            console.warn('[alarm] Autoplay blockiert — zeige Ton-starten-Button');
            _alarmAutoplayBlocked = true;
            // URL wieder vorne in die Queue legen damit sie bei Klick gespielt wird
            _alarmAudioQueue.unshift(url);
            alarmShowPlayBtn(true);
        } else {
            console.warn('[alarm] play() Fehler:', e.message, url);
            if (_alarmAudioPlaying) alarmAudioPlayNext();
        }
    });
}

function alarmEnsureAudioContext() {
    var AudioContextCtor = window.AudioContext || window.webkitAudioContext;
    if (!AudioContextCtor) return Promise.resolve(null);
    if (!_alarmFallbackContext) _alarmFallbackContext = new AudioContextCtor();
    if (_alarmFallbackContext.state === 'suspended') {
        return _alarmFallbackContext.resume().then(function() {
            return _alarmFallbackContext;
        });
    }
    return Promise.resolve(_alarmFallbackContext);
}

function alarmPrimeAutoplay() {
    if (_alarmAutoplayPrimed) return Promise.resolve(true);
    if (_alarmAutoplayPriming) return _alarmAutoplayPriming;
    _alarmAutoplayPriming = alarmEnsureAudioContext().then(function(ctx) {
        if (ctx) {
            var osc = ctx.createOscillator();
            var gain = ctx.createGain();
            gain.gain.value = 0.00001;
            osc.connect(gain);
            gain.connect(ctx.destination);
            osc.start();
            osc.stop(ctx.currentTime + 0.02);
        }
        if (!_alarmSilentMedia) {
            _alarmSilentMedia = new Audio('data:audio/wav;base64,UklGRlQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=');
            _alarmSilentMedia.preload = 'auto';
            _alarmSilentMedia.muted = true;
            _alarmSilentMedia.playsInline = true;
        }
        return _alarmSilentMedia.play().then(function() {
            _alarmSilentMedia.pause();
            _alarmSilentMedia.currentTime = 0;
            _alarmAutoplayPrimed = true;
            return true;
        }).catch(function() {
            _alarmAutoplayPrimed = !!ctx;
            return _alarmAutoplayPrimed;
        });
    }).catch(function() {
        return false;
    }).then(function(ok) {
        _alarmAutoplayPriming = null;
        return ok;
    });
    return _alarmAutoplayPriming;
}

function alarmInitAutoplayUnlock() {
    function unlockHandler() {
        alarmPrimeAutoplay().then(function(ok) {
            if (!ok) return;
            document.removeEventListener('pointerdown', unlockHandler, true);
            document.removeEventListener('touchstart', unlockHandler, true);
            document.removeEventListener('keydown', unlockHandler, true);
            if (_alarmAutoplayBlocked) {
                alarmShowPlayBtn(false);
                _alarmAutoplayBlocked = false;
                if (_alarmPendingUrls.length > 0) {
                    _alarmAudioPlaying = true;
                    alarmAudioPlayNext();
                } else if (_alarmAudioPlaying) {
                    alarmFallbackStart();
                }
            }
        });
    }
    document.addEventListener('pointerdown', unlockHandler, true);
    document.addEventListener('touchstart', unlockHandler, true);
    document.addEventListener('keydown', unlockHandler, true);
}

function alarmFallbackBeep() {
    return alarmEnsureAudioContext().then(function(ctx) {
        if (!ctx) return;
        var osc = ctx.createOscillator();
        var gain = ctx.createGain();
        osc.type = 'square';
        osc.frequency.value = 880;
        gain.gain.setValueAtTime(0.0001, ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.18, ctx.currentTime + 0.01);
        gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.28);
        osc.connect(gain);
        gain.connect(ctx.destination);
        osc.start();
        osc.stop(ctx.currentTime + 0.3);
    }).catch(function(e) {
        if (e && e.name === 'NotAllowedError') {
            _alarmAutoplayBlocked = true;
            alarmShowPlayBtn(true);
        }
    });
}

function alarmFallbackStart() {
    _alarmPendingUrls = [];
    _alarmAudioPlaying = true;
    _alarmAutoplayBlocked = false;
    alarmShowPlayBtn(false);
    if (_alarmFallbackTimer) clearInterval(_alarmFallbackTimer);
    alarmFallbackBeep();
    _alarmFallbackTimer = setInterval(function() {
        if (_alarmAudioPlaying) alarmFallbackBeep();
    }, 1200);
}

function alarmAudioStop() {
    _alarmAudioPlaying    = false;
    _alarmAutoplayBlocked = false;
    alarmShowPlayBtn(false);
    if (_alarmFallbackTimer) {
        clearInterval(_alarmFallbackTimer);
        _alarmFallbackTimer = null;
    }
    if (_alarmCurrentAudio) {
        _alarmCurrentAudio.pause();
        _alarmCurrentAudio.src = '';
        _alarmCurrentAudio = null;
    }
    _alarmAudioQueue  = [];
    _alarmPendingUrls = [];
}

// "Ton starten"-Button — Nutzer-Geste entsperrt Autoplay
(function() {
    document.addEventListener('DOMContentLoaded', function() {
        alarmInitAutoplayUnlock();
        var btn = document.getElementById('alarmPlayAudioBtn');
        if (!btn) return;
        btn.addEventListener('click', function() {
            alarmPrimeAutoplay().finally(function() {
                alarmShowPlayBtn(false);
                _alarmAutoplayBlocked = false;
                if (_alarmPendingUrls.length > 0) {
                    _alarmAudioPlaying = true;
                    alarmAudioPlayNext();
                } else {
                    alarmFallbackStart();
                }
            });
        });
    });
})();

// ─── Alarm-Klingel-Overlay ──────────────────────────────────────────────────
var _alarmRingingActive = false;

function alarmStorageRead(key) {
    try {
        var raw = localStorage.getItem(key);
        return raw ? JSON.parse(raw) : {};
    } catch (e) {
        return {};
    }
}

function alarmStorageWrite(key, value) {
    try {
        localStorage.setItem(key, JSON.stringify(value || {}));
    } catch (e) {}
}

function alarmPad(num) {
    return String(num).padStart(2, '0');
}

function alarmDateKey(date) {
    return date.getFullYear() + '-' + alarmPad(date.getMonth() + 1) + '-' + alarmPad(date.getDate());
}

function alarmTimeKey(date) {
    return alarmPad(date.getHours()) + ':' + alarmPad(date.getMinutes());
}

function alarmOccurrenceKey(alarm, date) {
    return String(alarm.id) + '_' + alarmDateKey(date) + '_' + (alarm.weckzeit || '00:00').substring(0, 5);
}

function alarmReadHandledMap() {
    var raw = alarmStorageRead(ALARM_LOCAL_HANDLED_KEY);
    var now = Date.now();
    var cleaned = {};
    Object.keys(raw).forEach(function(key) {
        var ts = Number(raw[key] || 0);
        if (ts > now - (3 * 24 * 60 * 60 * 1000)) cleaned[key] = ts;
    });
    alarmStorageWrite(ALARM_LOCAL_HANDLED_KEY, cleaned);
    return cleaned;
}

function alarmReadSnoozeMap() {
    var raw = alarmStorageRead(ALARM_LOCAL_SNOOZE_KEY);
    var now = Date.now();
    var cleaned = {};
    Object.keys(raw).forEach(function(key) {
        var until = Number(raw[key] || 0);
        if (until > now) cleaned[key] = until;
    });
    alarmStorageWrite(ALARM_LOCAL_SNOOZE_KEY, cleaned);
    return cleaned;
}

function alarmMarkHandled(occurrenceKey) {
    var handled = alarmReadHandledMap();
    handled[occurrenceKey] = Date.now();
    alarmStorageWrite(ALARM_LOCAL_HANDLED_KEY, handled);
}

function alarmIsHandled(occurrenceKey) {
    var handled = alarmReadHandledMap();
    return !!handled[occurrenceKey];
}

function alarmSetSnooze(alarmId, untilTs) {
    var snoozes = alarmReadSnoozeMap();
    snoozes[String(alarmId)] = untilTs;
    alarmStorageWrite(ALARM_LOCAL_SNOOZE_KEY, snoozes);
}

function alarmClearSnooze(alarmId) {
    var snoozes = alarmReadSnoozeMap();
    delete snoozes[String(alarmId)];
    alarmStorageWrite(ALARM_LOCAL_SNOOZE_KEY, snoozes);
}

function alarmIsSnoozed(alarmId) {
    var snoozes = alarmReadSnoozeMap();
    return Number(snoozes[String(alarmId)] || 0) > Date.now();
}

function alarmFindById(id) {
    id = Number(id);
    for (var i = 0; i < weckerListeData.length; i++) {
        if (Number(weckerListeData[i].id) === id) return weckerListeData[i];
    }
    return null;
}

function alarmResetSnoozeButton() {
    var btn = document.getElementById('alarmSnoozeBtn');
    if (!btn) return;
    btn.disabled = false;
    btn.innerHTML = ALARM_SNOOZE_LABEL_HTML;
}

function alarmSetOverlayContent(data) {
    var overlay = document.getElementById('alarmRingingOverlay');
    if (!overlay) return;
    var timeEl = document.getElementById('alarmRingingTime');
    var titleEl = document.getElementById('alarmRingingTitle');
    var coverImg = document.getElementById('alarmCoverImg');
    var fallback = document.getElementById('alarmCoverFallback');

    if (timeEl) timeEl.textContent = data.alarm_time || '–';
    if (titleEl) titleEl.textContent = data.title || 'Wecker klingelt';

    if (coverImg) {
        if (data.cover_url) {
            coverImg.src = data.cover_url;
            coverImg.style.display = '';
            if (fallback) fallback.style.display = 'none';
        } else {
            coverImg.removeAttribute('src');
            coverImg.style.display = 'none';
            if (fallback) fallback.style.display = 'flex';
        }
    }

    overlay.classList.add('active');
    overlay.setAttribute('aria-hidden', 'false');
    alarmResetSnoozeButton();
}

function alarmHideOverlay() {
    var overlay = document.getElementById('alarmRingingOverlay');
    if (!overlay) return;
    overlay.classList.remove('active');
    overlay.setAttribute('aria-hidden', 'true');
    alarmResetSnoozeButton();
}

function alarmSetOverlayTitle(title) {
    var titleEl = document.getElementById('alarmRingingTitle');
    if (titleEl) titleEl.textContent = title || 'Wecker klingelt';
}

function alarmFetchPreviewText(alarm) {
    var fallbackTitle = (alarm && alarm.sprachtext ? String(alarm.sprachtext) : '').trim() || 'Wecker klingelt';
    if (!alarm || !alarm.sprachtext || !String(alarm.sprachtext).trim()) {
        return Promise.resolve(fallbackTitle);
    }
    var fd = new FormData();
    fd.set('action', 'preview');
    fd.set('text', String(alarm.sprachtext));
    fd.set('id', String(alarm.id || '0'));
    if (alarm.weckzeit) fd.set('weckzeit', String(alarm.weckzeit));
    return fetch(API_WECKER, { method: 'POST', body: fd })
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (data && data.ok && data.preview !== undefined && String(data.preview).trim()) {
                return String(data.preview).trim();
            }
            return fallbackTitle;
        })
        .catch(function() {
            return fallbackTitle;
        });
}

function alarmFetchLocalAudio(alarm, ringDate) {
    if (!alarm || !alarm.id) return Promise.resolve(null);
    var fd = new FormData();
    fd.set('action', 'alarm_audio');
    fd.set('id', String(alarm.id));
    if (ringDate) fd.set('ring_date', String(ringDate));
    return fetch(API_WECKER, { method: 'POST', body: fd })
        .then(function(r) { return r.json(); })
        .then(function(data) {
            return data && data.ok ? data : null;
        })
        .catch(function() {
            return null;
        });
}

function alarmMaybeNotify(alarm) {
    try {
        if ('Notification' in window && Notification.permission === 'granted') {
            new Notification('Wecker', {
                body: (alarm.sprachtext || '').trim() || ('Alarm um ' + (alarm.weckzeit || '00:00'))
            });
        }
    } catch (e) {}
    try {
        if (navigator.vibrate) navigator.vibrate([250, 120, 250, 120, 250]);
    } catch (e) {}
}

function alarmMatchesNow(alarm, now, todayKey, weekday, timeKey) {
    if (!alarm || !alarm.aktiv) return false;
    if ((alarm.weckzeit || '00:00').substring(0, 5) !== timeKey) return false;
    if (alarm.wiederholung === 'einmal') return alarm.weckdatum === todayKey;
    if (alarm.wiederholung === 'woechentlich') {
        return Array.isArray(alarm.wochentage) && alarm.wochentage.indexOf(weekday) !== -1;
    }
    return true;
}

function getDueLocalAlarm(now) {
    var todayKey = alarmDateKey(now);
    var weekday = now.getDay() || 7;
    var timeKey = alarmTimeKey(now);
    for (var i = 0; i < weckerListeData.length; i++) {
        var alarm = weckerListeData[i];
        if (!alarmMatchesNow(alarm, now, todayKey, weekday, timeKey)) continue;
        if (alarmIsSnoozed(alarm.id)) continue;
        var occurrenceKey = alarmOccurrenceKey(alarm, now);
        if (alarmIsHandled(occurrenceKey)) continue;
        if (_alarmLocalCurrent && _alarmLocalCurrent.occurrenceKey === occurrenceKey) continue;
        return {
            alarm: alarm,
            occurrenceKey: occurrenceKey,
            ringDate: todayKey + ' ' + timeKey
        };
    }
    return null;
}

function triggerLocalAlarm(match) {
    if (!match || !match.alarm) return;
    var alarm = match.alarm;
    var alarmId = Number(alarm.id);
    _alarmLocalCurrent = {
        alarmId: alarmId,
        occurrenceKey: match.occurrenceKey
    };
    alarmClearSnooze(alarm.id);
    alarmSetOverlayContent({
        alarm_time: (alarm.weckzeit || '00:00').substring(0, 5),
        title: (alarm.sprachtext || '').trim() || 'Wecker klingelt',
        cover_url: ''
    });
    alarmMaybeNotify(alarm);
    alarmFetchPreviewText(alarm).then(function(title) {
        if (_alarmLocalCurrent && Number(_alarmLocalCurrent.alarmId) === alarmId) {
            alarmSetOverlayTitle(title);
        }
    });
    alarmFetchLocalAudio(alarm, match.ringDate).then(function(data) {
        if (!_alarmLocalCurrent || Number(_alarmLocalCurrent.alarmId) !== alarmId || _alarmLocalCurrent.occurrenceKey !== match.occurrenceKey) {
            return;
        }
        if (data) {
            alarmSetOverlayContent({
                alarm_time: data.alarm_time || (alarm.weckzeit || '00:00').substring(0, 5),
                title: (data.tts_text || alarm.sprachtext || '').trim() || 'Wecker klingelt',
                cover_url: data.cover_url || ''
            });
            alarmAudioStart(data.audio_urls || []);
        } else {
            alarmAudioStart([]);
        }
    });
}

function checkLocalAlarmRinging() {
    if (_alarmRingingActive || _alarmLocalCurrent) return;
    var match = getDueLocalAlarm(new Date());
    if (match) triggerLocalAlarm(match);
}

function checkAlarmRinging() {
    fetch(API_WECKER + '?action=alarm_state')
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (!data.ok) return;

            if (data.ringing && (!_alarmRingingActive || _alarmLocalCurrent)) {
                _alarmRingingActive = true;
                _alarmLocalCurrent = null;
                var alarm = alarmFindById(data.alarm_id);
                alarmSetOverlayContent({
                    alarm_time: data.alarm_time || '–',
                    title: (data.tts_text || (alarm && alarm.sprachtext) || '').trim() || 'Wecker klingelt',
                    cover_url: data.cover_url || ''
                });
                alarmAudioStart(data.audio_urls || []);
            } else if (!data.ringing && _alarmRingingActive) {
                _alarmRingingActive = false;
                alarmAudioStop();
                alarmHideOverlay();
            }
        })
        .catch(function() {});
}

function alarmSnooze() {
    var btn = document.getElementById('alarmSnoozeBtn');
    if (btn) { btn.disabled = true; btn.textContent = 'Snoozed…'; }
    alarmAudioStop();

    if (_alarmLocalCurrent) {
        alarmSetSnooze(_alarmLocalCurrent.alarmId, Date.now() + (5 * 60 * 1000));
        _alarmLocalCurrent = null;
        alarmHideOverlay();
        alarmResetSnoozeButton();
        return;
    }

    fetch(API_WECKER + '?action=snooze&minutes=5', { method: 'POST' })
        .then(function(r) { return r.json(); })
        .then(function() {
            _alarmRingingActive = false;
            alarmHideOverlay();
        })
        .catch(function() { if (btn) { btn.disabled = false; } });
}

function alarmStop() {
    var btn = document.getElementById('alarmStopBtn');
    if (btn) { btn.disabled = true; }
    alarmAudioStop();

    if (_alarmLocalCurrent) {
        alarmMarkHandled(_alarmLocalCurrent.occurrenceKey);
        alarmClearSnooze(_alarmLocalCurrent.alarmId);
        _alarmLocalCurrent = null;
        alarmHideOverlay();
        if (btn) btn.disabled = false;
        return;
    }

    fetch(API_WECKER + '?action=stop_alarm', { method: 'POST' })
        .then(function(r) { return r.json(); })
        .then(function() {
            _alarmRingingActive = false;
            alarmHideOverlay();
            if (btn) { btn.disabled = false; }
        })
        .catch(function() { if (btn) { btn.disabled = false; } });
}

checkLocalAlarmRinging();
setInterval(checkLocalAlarmRinging, 1000);
setInterval(checkAlarmRinging, 1000);

function toggleWochentageVisibility() {
    const w = document.getElementById('editWiederholung').value;
    document.getElementById('rowWochentage').style.display = w === 'woechentlich' ? 'block' : 'none';
    document.getElementById('rowWeckdatum').style.display = w === 'einmal' ? 'block' : 'none';
}

function editWecker(id) {
    var overlay = document.getElementById('editOverlay');
    var loading = document.getElementById('editOverlayLoading');
    var content = document.getElementById('editBoxContent');
    if (loading) loading.style.display = 'flex';
    if (content) content.style.display = 'none';
    overlay.style.display = 'flex';
    fetch(API_WECKER + '?action=get&id=' + encodeURIComponent(id))
        .then(r => r.json())
        .then(data => {
            if (loading) loading.style.display = 'none';
            if (content) content.style.display = 'block';
            if (!data.ok) { alert(data.error || 'Fehler'); closeEdit(); return; }
            const w = data.wecker;
            document.getElementById('editTitle').textContent = 'Wecker bearbeiten';
            document.getElementById('editId').value = w.id;
            document.getElementById('editWeckzeit').value = (w.weckzeit || '').substring(0, 5);
            document.getElementById('editWeckdatum').value = w.weckdatum || '';
            document.getElementById('editWiederholung').value = w.wiederholung || 'taeglich';
            document.getElementById('editSprachtext').value = w.sprachtext || '';
            var ttsVol = (w.lautstaerke_tts !== undefined && w.lautstaerke_tts !== null && w.lautstaerke_tts !== '') ? Number(w.lautstaerke_tts) : 100;
            var musikVol = (w.lautstaerke_musik !== undefined && w.lautstaerke_musik !== null && w.lautstaerke_musik !== '') ? Number(w.lautstaerke_musik) : 80;
            var duck = (w.musik_ducking_prozent !== undefined && w.musik_ducking_prozent !== null && w.musik_ducking_prozent !== '') ? Number(w.musik_ducking_prozent) : 20;
            document.getElementById('editLautstaerkeTts').value = ttsVol;
            document.getElementById('editLautstaerkeTtsVal').textContent = ttsVol;
            document.getElementById('editLautstaerkeMusik').value = musikVol;
            document.getElementById('editLautstaerkeMusikVal').textContent = musikVol;
            document.getElementById('editMusikDucking').value = duck;
            document.getElementById('editMusikDuckingVal').textContent = duck;
            const wochentage = w.wochentage || [];
            document.querySelectorAll('#editForm input[name="wochentage[]"]').forEach(cb => {
                cb.checked = wochentage.indexOf(parseInt(cb.value, 10)) !== -1;
            });
            syncWochentageAktivKlasse();
            toggleWochentageVisibility();
            audioList = Array.isArray(w.audio) ? w.audio.map(function(a) {
                var pos = a.position === 'gleichzeitig' || a.position === 'danach' ? a.position : 'danach';
                return { datei: a.datei, titel: a.titel || null, position: pos };
            }) : [];
            renderAudioLists();
            document.getElementById('btnDeleteWecker').style.display = '';
            document.getElementById('editOverlay').style.display = 'flex';
            if (typeof window.sprachtextToggleShowPlaceholder === 'function') window.sprachtextToggleShowPlaceholder();
        })
        .catch(function() {
            if (loading) loading.style.display = 'none';
            if (content) content.style.display = 'block';
            closeEdit();
            alert('Fehler beim Laden');
        });
}

function neuWecker() {
    document.getElementById('editOverlayLoading').style.display = 'none';
    document.getElementById('editBoxContent').style.display = 'block';
    document.getElementById('editTitle').textContent = 'Neuer Wecker';
    document.getElementById('editId').value = '0';
    document.getElementById('editWeckzeit').value = '07:00';
    document.getElementById('editWeckdatum').value = '';
    document.getElementById('editWiederholung').value = 'taeglich';
    document.getElementById('editSprachtext').value = '';
    document.getElementById('editLautstaerkeTts').value = 100;
    document.getElementById('editLautstaerkeTtsVal').textContent = '100';
    document.getElementById('editLautstaerkeMusik').value = 80;
    document.getElementById('editLautstaerkeMusikVal').textContent = '80';
    document.getElementById('editMusikDucking').value = 20;
    document.getElementById('editMusikDuckingVal').textContent = '20';
    document.querySelectorAll('#editForm input[name="wochentage[]"]').forEach(cb => { cb.checked = false; });
    syncWochentageAktivKlasse();
    audioList = [];
    renderAudioLists();
    toggleWochentageVisibility();
    document.getElementById('btnDeleteWecker').style.display = 'none';
    document.getElementById('editOverlay').style.display = 'flex';
    if (typeof window.sprachtextToggleShowPlaceholder === 'function') window.sprachtextToggleShowPlaceholder();
}

function closeEdit() {
    hideTouchKeyboard();
    document.getElementById('editOverlay').style.display = 'none';
}

function toggleWecker(id, aktiv) {
    const card = document.querySelector('.wecker[data-id="' + id + '"]');
    const checkbox = card ? card.querySelector('input[type="checkbox"]') : null;
    const prev = !aktiv;
    if (card) card.classList.toggle('inaktiv', !aktiv);
    fetch(API_WECKER + '?action=toggle&id=' + encodeURIComponent(id) + '&aktiv=' + (aktiv ? '1' : '0'))
        .then(r => r.json())
        .then(data => {
            if (!data.ok && checkbox) { checkbox.checked = prev; if (card) card.classList.toggle('inaktiv', prev); alert(data.error || 'Fehler'); }
            else if (data.ok) {
                weckerListeData = weckerListeData.map(function(w) { return w.id == id ? Object.assign({}, w, { aktiv: aktiv ? 1 : 0 }) : w; });
                _weckerLiveHash = JSON.stringify(weckerListeData);
                updateHeader();
            }
        })
        .catch(() => { if (checkbox) checkbox.checked = prev; if (card) card.classList.toggle('inaktiv', prev); alert('Netzwerkfehler'); });
}

function saveWecker(e) {
    e.preventDefault();
    document.getElementById('editAudio').value = JSON.stringify(audioList);
    const form = document.getElementById('editForm');
    const fd = new FormData(form);
    fd.set('action', 'save');
    fetch(API_WECKER, { method: 'POST', body: fd })
        .then(r => r.json())
        .then(data => {
            if (!data.ok) { alert(data.error || 'Fehler'); return; }
            closeEdit();
            _weckerLiveHash = null;
            refreshWeckerData();
        })
        .catch(() => alert('Fehler beim Speichern'));
    return false;
}

function deleteWecker() {
    const id = document.getElementById('editId').value;
    if (!id || id === '0') return;
    if (!confirm('Wecker wirklich löschen?')) return;
    const fd = new FormData();
    fd.set('action', 'delete');
    fd.set('id', id);
    fetch(API_WECKER, { method: 'POST', body: fd })
        .then(r => r.json())
        .then(data => {
            if (!data.ok) { alert(data.error || 'Fehler'); return; }
            closeEdit();
            _weckerLiveHash = null;
            refreshWeckerData();
        })
        .catch(() => alert('Fehler beim Löschen'));
}

var SETTINGS_LS_KEY = 'wecker-settings-cache';
var SETTINGS_LS_TTL = 5 * 60 * 1000;
function settingsCacheGet() {
    try {
        var raw = localStorage.getItem(SETTINGS_LS_KEY);
        if (!raw) return null;
        var c = JSON.parse(raw);
        if (!c || Date.now() - c.ts > SETTINGS_LS_TTL) return null;
        return c.data;
    } catch (e) { return null; }
}
function settingsCacheSet(data) {
    try { localStorage.setItem(SETTINGS_LS_KEY, JSON.stringify({ ts: Date.now(), data: data })); } catch (e) {}
}

function fillSettingsForm(e) {
    document.getElementById('settingTtsName').value = e.tts_name || '';
    document.getElementById('settingTtsProvider').value = (e.tts_provider === 'openai' ? 'openai' : 'piper');
    document.getElementById('settingTtsModell').value = e.tts_modell || 'de_DE-thorsten-high';
    document.getElementById('settingTtsOpenaiApiKey').value = e.tts_openai_api_key || '';
    document.getElementById('settingTtsOpenaiModel').value = e.tts_openai_model || 'tts-1';
    document.getElementById('settingTtsOpenaiVoice').value = e.tts_openai_voice || 'alloy';
    updateTtsProviderVisibility();
    document.getElementById('settingZeitzone').value = e.zeitzone || 'Europe/Berlin';
    document.getElementById('settingWetterOrt').value = e.wetter_ort || '';
    document.getElementById('settingSleepModeSekunden').value = e.sleep_mode_sekunden !== undefined && e.sleep_mode_sekunden !== '' ? e.sleep_mode_sekunden : '0';
    var accent = (e.accent_color || '').trim();
    document.getElementById('settingAccentColor').value = accent && /^#[0-9a-fA-F]{3,6}$/.test(accent) ? accent : '#2563eb';
    document.getElementById('settingAccentColorHex').value = accent || '';
}

function openSettings() {
    if (sleepModeVisible) hideSleepMode();
    var overlay = document.getElementById('settingsOverlay');
    var loading = document.getElementById('settingsOverlayLoading');
    overlay.style.display = 'flex';
    document.getElementById('settingsMsg').textContent = '';
    showSettingsTab('settingsTabAllgemein');
    var cached = settingsCacheGet();
    if (cached) {
        fillSettingsForm(cached);
        if (loading) loading.style.display = 'none';
    } else {
        if (loading) loading.style.display = 'flex';
    }
    fetch(API_EINSTELLUNGEN)
        .then(r => r.json())
        .then(data => {
            if (loading) loading.style.display = 'none';
            if (!data.ok) return;
            const e = data.einstellungen || {};
            settingsCacheSet(e);
            fillSettingsForm(e);
        })
        .catch(function() {
            if (loading) loading.style.display = 'none';
            if (!cached) {
                document.getElementById('settingsMsg').textContent = 'Fehler beim Laden.';
                document.getElementById('settingsMsg').className = 'settings-msg error';
            }
        });
    loadTtsCustomList();
}

function showSettingsTab(tabId) {
    document.querySelectorAll('.settings-tab-panel').forEach(function(p) { p.classList.remove('active'); });
    document.querySelectorAll('.settings-tabs button[role="tab"]').forEach(function(b) {
        b.classList.remove('active');
        b.setAttribute('aria-selected', b.getAttribute('data-tab') === tabId ? 'true' : 'false');
    });
    var panel = document.getElementById(tabId);
    var btn = document.querySelector('.settings-tabs button[data-tab="' + tabId + '"]');
    if (panel) panel.classList.add('active');
    if (btn) btn.classList.add('active');
}

function closeSettings(ev) {
    if (ev && ev.target !== ev.currentTarget) return;
    hideTouchKeyboard();
    document.getElementById('settingsOverlay').style.display = 'none';
}

function openSpieleMenu() {
    var el = document.getElementById('spieleMenuOverlay');
    if (!el) return;
    el.style.display = 'flex';
    setTimeout(function() {
        var firstGame = el.querySelector('.spiele-btn');
        if (firstGame) firstGame.focus();
    }, 0);
}
function closeSpieleMenu(ev) {
    if (ev && ev.target !== ev.currentTarget) return;
    var el = document.getElementById('spieleMenuOverlay');
    if (el) el.style.display = 'none';
}
document.addEventListener('keydown', function(e) {
    if (e.key !== 'Escape') return;
    var el = document.getElementById('spieleMenuOverlay');
    if (el && el.style.display === 'flex') closeSpieleMenu();
});

(function initWeckerLautstaerkeSliders() {
    var ttsEl = document.getElementById('editLautstaerkeTts');
    var musikEl = document.getElementById('editLautstaerkeMusik');
    var duckEl = document.getElementById('editMusikDucking');
    if (ttsEl) ttsEl.addEventListener('input', function() { (document.getElementById('editLautstaerkeTtsVal') || {}).textContent = this.value; });
    if (musikEl) musikEl.addEventListener('input', function() { (document.getElementById('editLautstaerkeMusikVal') || {}).textContent = this.value; });
    if (duckEl) duckEl.addEventListener('input', function() { (document.getElementById('editMusikDuckingVal') || {}).textContent = this.value; });
})();

function updateTtsProviderVisibility() {
    var provider = (document.getElementById('settingTtsProvider') || {}).value || 'piper';
    var piperEl = document.getElementById('ttsPiperOptions');
    var openaiEl = document.getElementById('ttsOpenaiOptions');
    if (piperEl) piperEl.style.display = provider === 'piper' ? '' : 'none';
    if (openaiEl) openaiEl.style.display = provider === 'openai' ? '' : 'none';
}
var settingTtsProviderEl = document.getElementById('settingTtsProvider');
if (settingTtsProviderEl) settingTtsProviderEl.addEventListener('change', updateTtsProviderVisibility);

(function initAccentColor() {
    var colorEl = document.getElementById('settingAccentColor');
    var hexEl = document.getElementById('settingAccentColorHex');
    var resetEl = document.getElementById('settingAccentReset');
    if (!colorEl || !hexEl) return;
    colorEl.addEventListener('input', function() {
        var hex = this.value;
        hexEl.value = hex;
        applyAccentColor(hex);
    });
    hexEl.addEventListener('input', function() {
        var v = this.value.trim();
        if (v === '') { applyAccentColor(''); return; }
        if (/^#[0-9a-fA-F]{3,6}$/.test(v)) {
            colorEl.value = v.length === 4 ? '#' + v[1] + v[1] + v[2] + v[2] + v[3] + v[3] : v;
            applyAccentColor(v);
        }
    });
    if (resetEl) resetEl.addEventListener('click', function() {
        colorEl.value = '#2563eb';
        hexEl.value = '';
        applyAccentColor('');
    });
})();

function loadTtsCustomList() {
    fetch(API_WECKER + '?action=tts_custom_list')
        .then(r => r.json())
        .then(data => {
            const list = document.getElementById('ttsCustomList');
            list.innerHTML = '';
            if (!data.ok || !data.items || data.items.length === 0) {
                list.innerHTML = '<p class="platzhalter-hint">Keine benutzerdefinierten Platzhalter.</p>';
                return;
            }
            data.items.forEach(item => {
                const div = document.createElement('div');
                div.className = 'tts-custom-item';
                div.innerHTML = '<span><code>' + escapeHtml(item.platzhalter) + '</code> → ' + escapeHtml(item.ersetzungstext) + '</span>' +
                    '<div><button type="button" class="header-btn btn-small" onclick="editCustom(' + item.id + ')">Bearb.</button> ' +
                    '<button type="button" class="header-btn btn-small danger" onclick="deleteCustom(' + item.id + ')">Löschen</button></div>';
                list.appendChild(div);
            });
        });
}

function escapeHtml(s) {
    const d = document.createElement('div');
    d.textContent = s;
    return d.innerHTML;
}

function editCustom(id) {
    fetch(API_WECKER + '?action=tts_custom_list')
        .then(r => r.json())
        .then(data => {
            if (!data.ok || !data.items) return;
            const item = data.items.find(i => parseInt(i.id, 10) === id);
            if (!item) return;
            document.getElementById('customId').value = item.id;
            document.getElementById('customPlatzhalter').value = (item.platzhalter || '').replace(/^\{\{|\}\}$/g, '');
            document.getElementById('customErsetzungstext').value = item.ersetzungstext || '';
            document.getElementById('customBeschreibung').value = item.beschreibung || '';
            document.getElementById('btnCustomSave').textContent = 'Speichern';
        });
}

function deleteCustom(id) {
    if (!confirm('Platzhalter löschen?')) return;
    const fd = new FormData();
    fd.set('action', 'tts_custom_delete');
    fd.set('id', id);
    fetch(API_WECKER, { method: 'POST', body: fd })
        .then(r => r.json())
        .then(data => {
            if (data.ok) loadTtsCustomList();
            else document.getElementById('settingsMsg').textContent = data.error || 'Fehler';
            document.getElementById('settingsMsg').className = 'settings-msg' + (data.ok ? '' : ' error');
        });
}

document.getElementById('btnCustomSave').addEventListener('click', function() {
    const id = document.getElementById('customId').value;
    const platzhalter = (document.getElementById('customPlatzhalter').value || '').trim();
    const ersetzungstext = (document.getElementById('customErsetzungstext').value || '').trim();
    const beschreibung = (document.getElementById('customBeschreibung').value || '').trim();
    if (!platzhalter) { document.getElementById('settingsMsg').textContent = 'Platzhalter eingeben'; document.getElementById('settingsMsg').className = 'settings-msg error'; return; }
    const fd = new FormData();
    fd.set('action', 'tts_custom_save');
    if (id && id !== '0') fd.set('id', id);
    fd.set('platzhalter', platzhalter);
    fd.set('ersetzungstext', ersetzungstext);
    fd.set('beschreibung', beschreibung);
    fd.set('aktiv', '1');
    fetch(API_WECKER, { method: 'POST', body: fd })
        .then(r => r.json())
        .then(data => {
            if (!data.ok) { document.getElementById('settingsMsg').textContent = data.error || 'Fehler'; document.getElementById('settingsMsg').className = 'settings-msg error'; return; }
            document.getElementById('customId').value = '0';
            document.getElementById('customPlatzhalter').value = '';
            document.getElementById('customErsetzungstext').value = '';
            document.getElementById('customBeschreibung').value = '';
            document.getElementById('btnCustomSave').textContent = 'Hinzufügen / Speichern';
            document.getElementById('settingsMsg').textContent = 'Gespeichert.';
            document.getElementById('settingsMsg').className = 'settings-msg';
            loadTtsCustomList();
        });
});

function saveSettings() {
    const fd = new FormData();
    fd.set('tts_name', document.getElementById('settingTtsName').value);
    fd.set('tts_provider', document.getElementById('settingTtsProvider').value);
    fd.set('tts_modell', document.getElementById('settingTtsModell').value);
    fd.set('tts_openai_api_key', document.getElementById('settingTtsOpenaiApiKey').value);
    fd.set('tts_openai_model', document.getElementById('settingTtsOpenaiModel').value);
    fd.set('tts_openai_voice', document.getElementById('settingTtsOpenaiVoice').value);
    fd.set('zeitzone', document.getElementById('settingZeitzone').value);
    fd.set('wetter_ort', document.getElementById('settingWetterOrt').value);
    fd.set('sleep_mode_sekunden', document.getElementById('settingSleepModeSekunden').value);
    var accentVal = (document.getElementById('settingAccentColorHex').value || '').trim();
    if (!/^#[0-9a-fA-F]{3,6}$/.test(accentVal)) accentVal = '';
    fd.set('accent_color', accentVal);
    fetch(API_EINSTELLUNGEN, { method: 'POST', body: fd })
        .then(r => r.json())
        .then(data => {
            const msg = document.getElementById('settingsMsg');
            msg.textContent = data.ok ? 'Einstellungen gespeichert.' : (data.error || 'Fehler');
            msg.className = 'settings-msg' + (data.ok ? '' : ' error');
            if (data.ok) {
                var newOrt = (document.getElementById('settingWetterOrt').value || '').trim();
                try {
                    var oldOrt = localStorage.getItem(WETTER_ORT_LS_KEY) || '';
                    if (newOrt !== oldOrt) { wetterCacheClear(); currentWetterOrt = newOrt; }
                    localStorage.setItem(WETTER_ORT_LS_KEY, newOrt);
                } catch (e) {}
                settingsCacheSet({
                    tts_name: document.getElementById('settingTtsName').value,
                    tts_provider: document.getElementById('settingTtsProvider').value,
                    tts_modell: document.getElementById('settingTtsModell').value,
                    tts_openai_api_key: '********',
                    tts_openai_model: document.getElementById('settingTtsOpenaiModel').value,
                    tts_openai_voice: document.getElementById('settingTtsOpenaiVoice').value,
                    zeitzone: document.getElementById('settingZeitzone').value,
                    wetter_ort: newOrt,
                    sleep_mode_sekunden: document.getElementById('settingSleepModeSekunden').value,
                    accent_color: (document.getElementById('settingAccentColorHex').value || '').trim(),
                });
                var v = parseInt(document.getElementById('settingSleepModeSekunden').value, 10);
                sleepModeSekunden = isNaN(v) ? 0 : Math.max(0, Math.min(86400, v));
                var ac = (document.getElementById('settingAccentColorHex').value || '').trim();
                applyAccentColor(/^#[0-9a-fA-F]{3,6}$/.test(ac) ? ac : '');
            }
        })
        .catch(() => {
            document.getElementById('settingsMsg').textContent = 'Netzwerkfehler';
            document.getElementById('settingsMsg').className = 'settings-msg error';
        });
}

var timerInterval = null, timerRest = 0;

function getTimerPresets() {
    try {
        var raw = localStorage.getItem('wecker-timer-presets');
        if (raw) {
            var arr = JSON.parse(raw);
            if (Array.isArray(arr) && arr.length >= 4) return arr.slice(0, 4).map(function(n) { return Math.max(1, Math.min(99, parseInt(n, 10) || 15)); });
        }
    } catch (e) {}
    return [15, 30, 45, 60];
}

function setTimerPresets(arr) {
    var four = (arr || []).slice(0, 4).map(function(n) { return Math.max(1, Math.min(99, parseInt(n, 10) || 15)); });
    while (four.length < 4) four.push([15, 30, 45, 60][four.length]);
    try { localStorage.setItem('wecker-timer-presets', JSON.stringify(four)); } catch (e) {}
    return four;
}

function initTimer() {
    var inpMin = document.getElementById('timerMin');
    var inpSek = document.getElementById('timerSek');
    var anzeige = document.getElementById('timerAnzeige');
    var btnStart = document.getElementById('timerStart');
    var btnStop = document.getElementById('timerStop');
    var setDiv = document.querySelector('.timer-set');
    var presetsContainer = document.getElementById('timerPresets');
    var editOverlay = document.getElementById('timerPresetEditOverlay');
    var editMin = document.getElementById('timerPresetEditMin');
    var editSave = document.getElementById('timerPresetEditSave');
    var editCancel = document.getElementById('timerPresetEditCancel');

    function showSet(show) { if (setDiv) setDiv.style.display = show ? 'flex' : 'none'; if (anzeige) anzeige.style.display = show ? 'none' : 'block'; if (btnStop) btnStop.style.display = show ? 'none' : 'inline-flex'; }

    function renderPresets() {
        if (!presetsContainer) return;
        var presets = getTimerPresets();
        presetsContainer.innerHTML = '';
        presets.forEach(function(minutes, index) {
            var btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'timer-preset-btn';
            btn.textContent = minutes + ' Min';
            btn.dataset.minutes = minutes;
            btn.dataset.index = index;
            btn.setAttribute('aria-label', minutes + ' Minuten. Lang drücken zum Bearbeiten.');
            var longPressTimer = null;
            function clearLongPress() {
                if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; }
            }
            btn.addEventListener('click', function(e) {
                if (longPressTimer) return;
                if (editOverlay && editOverlay.classList.contains('show')) return;
                if (timerInterval) return;
                inpMin.value = minutes;
                if (inpSek) inpSek.value = 0;
            });
            btn.addEventListener('contextmenu', function(e) {
                e.preventDefault();
                openPresetEdit(index);
            });
            btn.addEventListener('touchstart', function(e) {
                clearLongPress();
                longPressTimer = setTimeout(function() {
                    longPressTimer = null;
                    e.preventDefault();
                    openPresetEdit(index);
                }, 500);
            });
            btn.addEventListener('touchend', clearLongPress);
            btn.addEventListener('touchcancel', clearLongPress);
            presetsContainer.appendChild(btn);
        });
    }

    var editingPresetIndex = -1;
    function openPresetEdit(index) {
        var presets = getTimerPresets();
        if (index < 0 || index >= presets.length) return;
        editingPresetIndex = index;
        if (editMin) editMin.value = presets[index];
        if (editOverlay) editOverlay.classList.add('show');
    }
    function closePresetEdit() {
        editingPresetIndex = -1;
        if (editOverlay) editOverlay.classList.remove('show');
    }
    if (editOverlay) {
        editOverlay.addEventListener('click', function(e) { if (e.target === editOverlay) closePresetEdit(); });
    }
    if (editCancel) editCancel.addEventListener('click', closePresetEdit);
    if (editSave && editMin) editSave.addEventListener('click', function() {
        var val = Math.max(1, Math.min(99, parseInt(editMin.value, 10) || 15));
        var presets = getTimerPresets();
        if (editingPresetIndex >= 0 && editingPresetIndex < presets.length) {
            presets[editingPresetIndex] = val;
            setTimerPresets(presets);
            renderPresets();
        }
        closePresetEdit();
    });

    renderPresets();

    if (btnStart) btnStart.addEventListener('click', function() {
        if (timerInterval) return;
        var min = parseInt(inpMin.value, 10) || 0, sek = parseInt(inpSek.value, 10) || 0;
        timerRest = min * 60 + sek;
        if (timerRest <= 0) return;
        showSet(false);
        timerInterval = setInterval(function() {
            timerRest--;
            if (anzeige) anzeige.textContent = (Math.floor(timerRest / 60) < 10 ? '0' : '') + Math.floor(timerRest / 60) + ':' + (timerRest % 60 < 10 ? '0' : '') + (timerRest % 60);
            if (timerRest <= 0) { clearInterval(timerInterval); timerInterval = null; showSet(true); alert('Timer abgelaufen!'); }
        }, 1000);
    });
    if (btnStop) btnStop.addEventListener('click', function() {
        if (timerInterval) { clearInterval(timerInterval); timerInterval = null; showSet(true); }
    });
    showSet(true);
}

var currentWetterOrt = '';
var WETTER_LS_KEY = 'wecker-wetter-data';
var WETTER_ORT_LS_KEY = 'wecker-wetter-ort';
var WETTER_LS_TTL = 15 * 60 * 1000;
function wetterCacheGet(ort) {
    try {
        var raw = localStorage.getItem(WETTER_LS_KEY);
        if (!raw) return null;
        var c = JSON.parse(raw);
        if (!c || c.ort !== ort) return null;
        if (Date.now() - c.ts > WETTER_LS_TTL) return null;
        return c.data;
    } catch (e) { return null; }
}
function wetterCacheSet(ort, data) {
    try {
        localStorage.setItem(WETTER_LS_KEY, JSON.stringify({ ort: ort, ts: Date.now(), data: data }));
        localStorage.setItem(WETTER_ORT_LS_KEY, ort);
    } catch (e) {}
}
function wetterCacheClear() {
    try { localStorage.removeItem(WETTER_LS_KEY); } catch (e) {}
}
function loadWetterFromStorage() {
    var subtitle = document.getElementById('wetterSubtitle');
    var leftEl = document.getElementById('wetterAktuell');
    var centerEl = document.getElementById('wetterForecast');
    var rightEl = document.getElementById('wetterDetails');
    var cachedOrt = '';
    try { cachedOrt = localStorage.getItem(WETTER_ORT_LS_KEY) || ''; } catch (e) {}
    if (cachedOrt) {
        currentWetterOrt = cachedOrt;
        if (subtitle) subtitle.textContent = cachedOrt;
        fillWetterPanels(leftEl, centerEl, rightEl, cachedOrt);
    } else {
        if (leftEl) leftEl.innerHTML = '<p class="sleep-wetter-tag">Lade…</p>';
        if (centerEl) centerEl.innerHTML = '';
        if (rightEl) rightEl.innerHTML = '';
    }
    fetch(API_EINSTELLUNGEN).then(function(r) { return r.json(); }).then(function(data) {
        var ort = (data.einstellungen && data.einstellungen.wetter_ort) ? String(data.einstellungen.wetter_ort).trim() : '';
        try { localStorage.setItem(WETTER_ORT_LS_KEY, ort); } catch (e) {}
        if (ort !== currentWetterOrt) {
            currentWetterOrt = ort;
            if (subtitle) subtitle.textContent = ort || 'Ort in Einstellungen festlegen';
            fillWetterPanels(leftEl, centerEl, rightEl, ort);
        } else if (!cachedOrt) {
            currentWetterOrt = ort;
            if (subtitle) subtitle.textContent = ort || 'Ort in Einstellungen festlegen';
            fillWetterPanels(leftEl, centerEl, rightEl, ort);
        }
    }).catch(function() {
        if (!cachedOrt && leftEl) leftEl.innerHTML = '<p class="sleep-wetter-tag">Einstellungen konnten nicht geladen werden.</p>';
    });
}
function fetchWetterAnzeigen(ort) {
    fillWetterPanels(
        document.getElementById('wetterAktuell'),
        document.getElementById('wetterForecast'),
        document.getElementById('wetterDetails'),
        ort || currentWetterOrt
    );
}
function initWetter() {
    var btn = document.getElementById('wetterAktualisieren');
    if (btn) btn.addEventListener('click', function() {
        var ort = currentWetterOrt;
        if (!ort) { loadWetterFromStorage(); return; }
        fillWetterPanels(
            document.getElementById('wetterAktuell'),
            document.getElementById('wetterForecast'),
            document.getElementById('wetterDetails'),
            ort, true
        );
    });
}

function loadInfoTab() {
    var ipEl = document.getElementById('infoIp');
    if (ipEl) ipEl.textContent = '…';
    fetch(API_INFO).then(function(r) { return r.json(); }).then(function(data) {
        if (!data || !data.ok) return;
        if (ipEl) ipEl.textContent = (data.ip && data.ip !== '') ? data.ip : '–';
    }).catch(function() {
        if (ipEl) ipEl.textContent = '–';
    });
}

function loadDebugTab() {
    var timeEl   = document.getElementById('infoCronTime');
    var statusEl = document.getElementById('infoCronStatus');
    if (timeEl) timeEl.textContent = '…';
    if (statusEl) { statusEl.textContent = ''; statusEl.className = 'info-cron-status'; }
    updateRemoteWsDebugStatus();
    fetch(API_INFO).then(function(r) { return r.json(); }).then(function(data) {
        if (!data || !data.ok) return;
        var last = data.cron_last_activity || null;
        if (timeEl) {
            if (last) {
                try {
                    var d = new Date(last);
                    timeEl.textContent = isNaN(d.getTime()) ? last : d.toLocaleString('de-DE');
                } catch (e) { timeEl.textContent = last; }
            } else { timeEl.textContent = '–'; }
        }
        if (statusEl) {
            var status = data.cron_status || 'error';
            statusEl.classList.remove('status-ok', 'status-warn', 'status-error');
            statusEl.classList.add('status-' + status);
            if (status === 'ok')        statusEl.textContent = 'Cron läuft (letzter Lauf vor weniger als 1 Minute)';
            else if (status === 'warn') statusEl.textContent = 'Cron war vor 1–5 Minuten aktiv';
            else                        statusEl.textContent = 'Cron seit über 5 Minuten inaktiv oder nicht gestartet';
        }
    }).catch(function() {
        if (timeEl)   timeEl.textContent = 'Fehler';
        if (statusEl) { statusEl.className = 'info-cron-status status-error'; statusEl.textContent = 'Info konnte nicht geladen werden.'; }
    });
    loadCronJobList();
}

function loadCronJobList() {
    var listEl = document.getElementById('cronJobList');
    var hintEl = document.getElementById('cronJobListHint');
    if (!listEl) return;
    if (hintEl) hintEl.textContent = 'Lade Jobs…';
    fetch(API_INFO + '?action=list_jobs').then(function(r) { return r.json(); }).then(function(data) {
        if (hintEl) hintEl.remove();
        listEl.innerHTML = '';
        if (!data || !data.ok || !data.jobs || data.jobs.length === 0) {
            listEl.innerHTML = '<p class="platzhalter-hint">Keine Jobs gefunden.</p>';
            return;
        }
        data.jobs.forEach(function(job) {
            var item = document.createElement('div');
            item.className = 'cron-job-item';
            var meta = '';
            if (job.letzte_ausfuehrung) {
                try {
                    var d = new Date(job.letzte_ausfuehrung);
                    meta = 'Zuletzt: ' + (isNaN(d.getTime()) ? job.letzte_ausfuehrung : d.toLocaleString('de-DE'));
                } catch(e) { meta = 'Zuletzt: ' + job.letzte_ausfuehrung; }
            } else {
                meta = 'Noch nie ausgeführt';
            }
            if (job.interval_sekunden) {
                meta += ' · Intervall: ' + job.interval_sekunden + ' s';
            }
            item.innerHTML =
                '<div class="cron-job-info">' +
                    '<div class="cron-job-label">' + escapeHtml(job.label) + '</div>' +
                    '<div class="cron-job-meta" id="cronMeta_' + escapeHtml(job.name) + '">' + escapeHtml(meta) + '</div>' +
                '</div>' +
                '<button type="button" class="cron-job-run-btn" data-job="' + escapeHtml(job.name) + '">Starten</button>';
            listEl.appendChild(item);
        });
        listEl.querySelectorAll('.cron-job-run-btn').forEach(function(btn) {
            btn.addEventListener('click', function() {
                runSingleCronJob(btn.getAttribute('data-job'), btn);
            });
        });
    }).catch(function() {
        listEl.innerHTML = '<p class="platzhalter-hint">Jobs konnten nicht geladen werden.</p>';
    });
}

function runSingleCronJob(jobName, btn) {
    var origText = btn ? btn.textContent : '';
    if (btn) { btn.disabled = true; btn.textContent = 'Läuft…'; }
    fetch(API_INFO + '?action=run_job&job=' + encodeURIComponent(jobName))
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (btn) { btn.disabled = false; btn.textContent = origText; }
            var msg = document.getElementById('settingsMsg');
            if (msg) {
                msg.textContent = (data && data.ok) ? (data.message || 'Job ausgeführt.') : (data && data.message ? data.message : 'Fehler.');
                msg.className = 'settings-msg' + (data && data.ok ? '' : ' error');
            }
            if (data && data.ok) loadDebugTab();
        })
        .catch(function() {
            if (btn) { btn.disabled = false; btn.textContent = origText; }
            var msg = document.getElementById('settingsMsg');
            if (msg) { msg.textContent = 'Fehler beim Starten des Jobs.'; msg.className = 'settings-msg error'; }
        });
}

function formatBytes(b) {
    if (b === 0) return '0 B';
    if (b < 1024) return b + ' B';
    if (b < 1024 * 1024) return (b / 1024).toFixed(1) + ' KB';
    return (b / 1024 / 1024).toFixed(1) + ' MB';
}

function loadCleanupScan() {
    var resultEl = document.getElementById('cleanupResult');
    var btn = document.getElementById('btnCleanupScan');
    if (!resultEl) return;
    if (btn) { btn.disabled = true; btn.textContent = 'Scannt…'; }
    resultEl.style.display = 'block';
    resultEl.innerHTML = '<p class="platzhalter-hint">Analysiere…</p>';
    fetch(API_INFO + '?action=cleanup_scan')
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (btn) { btn.disabled = false; btn.textContent = 'Neu scannen'; }
            if (!data.ok) { resultEl.innerHTML = '<p class="platzhalter-hint" style="color:var(--danger,#e74c3c)">Fehler beim Scannen.</p>'; return; }
            renderCleanupResult(resultEl, data.items);
        })
        .catch(function() {
            if (btn) { btn.disabled = false; btn.textContent = 'Scannen'; }
            resultEl.innerHTML = '<p class="platzhalter-hint" style="color:var(--danger,#e74c3c)">Netzwerkfehler.</p>';
        });
}

function renderCleanupResult(el, items) {
    var backups = items.backups || {};
    var logs = items.logs || {};
    var zip = items.update_zip || {};
    var wetter = items.wetter_cache || {};
    var rows = '';

    // Backups
    var bCount = backups.count || 0;
    var bSize = backups.size || 0;
    rows += '<div class="cleanup-item">' +
        '<div class="cleanup-item-info">' +
        '<div class="cleanup-item-label">Backups (backup_*)</div>' +
        '<div class="cleanup-item-meta">' + bCount + ' Ordner &middot; ' + formatBytes(bSize) + '</div>' +
        '</div><div class="cleanup-item-actions">';
    if (bCount > 1) {
        rows += '<button class="cleanup-btn" onclick="runCleanupTarget(\'backups_except_newest\',this)">Älteste löschen</button>';
    }
    if (bCount > 0) {
        rows += '<button class="cleanup-btn danger" onclick="runCleanupTarget(\'backups_all\',this)">Alle löschen</button>';
    }
    if (bCount === 0) rows += '<span style="font-size:0.82rem;color:var(--text-muted)">–</span>';
    rows += '</div></div>';

    // Logs
    var lCount = logs.count || 0;
    var lSize = logs.size || 0;
    rows += '<div class="cleanup-item">' +
        '<div class="cleanup-item-info">' +
        '<div class="cleanup-item-label">Log-Dateien</div>' +
        '<div class="cleanup-item-meta">' + lCount + ' Datei(en) &middot; ' + formatBytes(lSize) + '</div>' +
        '</div><div class="cleanup-item-actions">';
    if (lCount > 0) {
        rows += '<button class="cleanup-btn" onclick="runCleanupTarget(\'logs\',this)">Löschen</button>';
    } else {
        rows += '<span style="font-size:0.82rem;color:var(--text-muted)">–</span>';
    }
    rows += '</div></div>';

    // Update-ZIP
    var zipExists = zip.exists || false;
    var zipSize = zip.size || 0;
    rows += '<div class="cleanup-item">' +
        '<div class="cleanup-item-info">' +
        '<div class="cleanup-item-label">update.zip</div>' +
        '<div class="cleanup-item-meta">' + (zipExists ? formatBytes(zipSize) : 'nicht vorhanden') + '</div>' +
        '</div><div class="cleanup-item-actions">';
    if (zipExists) {
        rows += '<button class="cleanup-btn" onclick="runCleanupTarget(\'update_zip\',this)">Löschen</button>';
    } else {
        rows += '<span style="font-size:0.82rem;color:var(--text-muted)">–</span>';
    }
    rows += '</div></div>';

    // Wetter-Cache
    var wCount = wetter.count || 0;
    var wSize = wetter.size || 0;
    rows += '<div class="cleanup-item">' +
        '<div class="cleanup-item-info">' +
        '<div class="cleanup-item-label">Wetter-Cache</div>' +
        '<div class="cleanup-item-meta">' + wCount + ' Datei(en) &middot; ' + formatBytes(wSize) + '</div>' +
        '</div><div class="cleanup-item-actions">';
    if (wCount > 0) {
        rows += '<button class="cleanup-btn" onclick="runCleanupTarget(\'wetter_cache\',this)">Leeren</button>';
    } else {
        rows += '<span style="font-size:0.82rem;color:var(--text-muted)">–</span>';
    }
    rows += '</div></div>';

    el.innerHTML = '<div class="cleanup-list">' + rows + '</div>' +
        '<div id="cleanupLog" class="cleanup-log" style="display:none;"></div>';
}

function runCleanupTarget(target, btn) {
    var logEl = document.getElementById('cleanupLog');
    if (btn) { btn.disabled = true; btn.textContent = 'Läuft…'; }
    var fd = new FormData();
    fd.append('target', target);
    fetch(API_INFO + '?action=cleanup_run', { method: 'POST', body: fd })
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (btn) { btn.disabled = false; btn.textContent = btn.textContent.replace('Läuft…', btn._origText || 'Fertig'); }
            if (logEl) {
                logEl.style.display = 'block';
                var lines = (data.log && Array.isArray(data.log)) ? data.log : [data.message || (data.ok ? 'Fertig.' : 'Fehler.')];
                logEl.textContent = lines.join('\n');
                logEl.style.color = data.ok ? 'var(--text-muted)' : 'var(--danger,#e74c3c)';
            }
            if (data.ok) loadCleanupScan();
        })
        .catch(function() {
            if (btn) { btn.disabled = false; }
            if (logEl) { logEl.style.display = 'block'; logEl.textContent = 'Netzwerkfehler.'; logEl.style.color = 'var(--danger,#e74c3c)'; }
        });
}

function systemCommand(cmd) {
    var msgEl = document.getElementById('systemCmdMsg');
    var btnR = document.getElementById('btnReboot');
    var btnS = document.getElementById('btnShutdown');
    var label = cmd === 'reboot' ? 'Neu starten' : 'Herunterfahren';
    if (!confirm(label + ': Wirklich fortfahren?')) return;
    if (btnR) btnR.disabled = true;
    if (btnS) btnS.disabled = true;
    if (msgEl) { msgEl.style.display = 'block'; msgEl.textContent = label + ' wird ausgeführt…'; msgEl.style.color = ''; }
    var fd = new FormData();
    fd.append('cmd', cmd);
    fetch(API_INFO + '?action=system_cmd', { method: 'POST', body: fd })
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (btnR) btnR.disabled = false;
            if (btnS) btnS.disabled = false;
            if (msgEl) {
                msgEl.textContent = data.message || (data.ok ? 'Befehl ausgeführt.' : 'Fehler.');
                msgEl.style.color = data.ok ? '' : 'var(--danger,#e74c3c)';
            }
        })
        .catch(function() {
            if (btnR) btnR.disabled = false;
            if (btnS) btnS.disabled = false;
            if (msgEl) { msgEl.textContent = 'Netzwerkfehler.'; msgEl.style.color = 'var(--danger,#e74c3c)'; }
        });
}

function killFirefox() {
    var msgEl = document.getElementById('killFirefoxMsg');
    var btn = document.getElementById('btnKillFirefox');
    if (!confirm('Alle Firefox-Prozesse beenden?')) return;
    if (btn) btn.disabled = true;
    if (msgEl) { msgEl.style.display = 'block'; msgEl.textContent = 'Wird ausgeführt…'; msgEl.style.color = ''; }
    var fd = new FormData();
    fd.append('cmd', 'kill_firefox');
    fetch(API_INFO + '?action=system_cmd', { method: 'POST', body: fd })
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (btn) btn.disabled = false;
            if (msgEl) {
                msgEl.textContent = data.message || (data.ok ? 'Erledigt.' : 'Fehler.');
                msgEl.style.color = data.ok ? '' : 'var(--danger,#e74c3c)';
            }
        })
        .catch(function() {
            if (btn) btn.disabled = false;
            if (msgEl) { msgEl.textContent = 'Netzwerkfehler.'; msgEl.style.color = 'var(--danger,#e74c3c)'; }
        });
}

var UPDATE_LS_KEY = 'wecker-update-cache';
var UPDATE_LS_TTL = 20 * 60 * 1000;
function updateCacheGet() {
    try {
        var raw = localStorage.getItem(UPDATE_LS_KEY);
        if (!raw) return null;
        var c = JSON.parse(raw);
        if (!c || Date.now() - c.ts > UPDATE_LS_TTL) return null;
        return c;
    } catch (e) { return null; }
}
function updateCacheSet(current, list) {
    try { localStorage.setItem(UPDATE_LS_KEY, JSON.stringify({ ts: Date.now(), current: current, list: list })); } catch (e) {}
}
function updateCacheClear() {
    try { localStorage.removeItem(UPDATE_LS_KEY); } catch (e) {}
}

function fillUpdateTab(current, list) {
    var currentEl = document.getElementById('updateCurrentVersion');
    var selectEl = document.getElementById('updateVersionSelect');
    var hintEl = document.getElementById('updateVersionHint');
    var installBtn = document.getElementById('updateInstallBtn');
    if (!selectEl || !installBtn) return;
    if (currentEl) currentEl.textContent = (current && current.ok && current.version != null) ? current.version : '–';
    var versionen = (list && list.ok && list.versionen) ? list.versionen : [];
    selectEl.innerHTML = '';
    if (versionen.length === 0) {
        selectEl.innerHTML = '<option value="">Keine Versionen geladen</option>';
        installBtn.disabled = true;
    } else {
        versionen.forEach(function(v) {
            var opt = document.createElement('option');
            opt.value = v.version || '';
            opt.textContent = (v.version || '–') + ((v.time || v.date) ? ' (' + (v.time || v.date) + ')' : '');
            opt.setAttribute('data-commit', v.commit || '');
            selectEl.appendChild(opt);
        });
        installBtn.disabled = false;
    }
    selectEl.removeEventListener('change', selectEl._hintHandler);
    selectEl._hintHandler = function() {
        var opt = selectEl.options[selectEl.selectedIndex];
        var commit = opt && opt.getAttribute('data-commit');
        if (hintEl) hintEl.textContent = commit ? 'Commit: ' + commit : '';
    };
    selectEl.addEventListener('change', selectEl._hintHandler);
    selectEl._hintHandler();
}

function loadUpdateTab() {
    var selectEl = document.getElementById('updateVersionSelect');
    var installBtn = document.getElementById('updateInstallBtn');
    if (!selectEl || !installBtn) return;
    var cached = updateCacheGet();
    if (cached) {
        fillUpdateTab(cached.current, cached.list);
    } else {
        selectEl.innerHTML = '<option value="">Lade…</option>';
        installBtn.disabled = true;
        var currentEl = document.getElementById('updateCurrentVersion');
        if (currentEl) currentEl.textContent = '…';
    }
    Promise.all([
        fetch(API_UPDATE + '?action=current').then(function(r) { return r.json(); }),
        fetch(API_UPDATE + '?action=list').then(function(r) { return r.json(); })
    ]).then(function(results) {
        var current = results[0];
        var list = results[1];
        updateCacheSet(current, list);
        fillUpdateTab(current, list);
    }).catch(function() {
        if (!cached) {
            var currentEl = document.getElementById('updateCurrentVersion');
            if (currentEl) currentEl.textContent = '–';
            selectEl.innerHTML = '<option value="">Fehler beim Laden</option>';
        }
    });
}

function initUpdateTab() {
    var installBtn = document.getElementById('updateInstallBtn');
    var logWrap = document.getElementById('updateLogWrap');
    var logEl = document.getElementById('updateLog');
    var reloadLink = document.getElementById('updateReloadLink');
    var versionEl = document.getElementById('updateCurrentVersion');
    if (versionEl) {
        var versionTapCount = 0;
        var versionTapReset = null;
        versionEl.addEventListener('click', function() {
            versionTapCount++;
            if (versionTapReset) clearTimeout(versionTapReset);
            versionTapReset = setTimeout(function() { versionTapCount = 0; versionTapReset = null; }, 1500);
            if (versionTapCount >= 5) {
                versionTapCount = 0;
                if (versionTapReset) clearTimeout(versionTapReset);
                openSpieleMenu();
            }
        });
    }
    if (!installBtn) return;
    installBtn.addEventListener('click', function() {
        var selectEl = document.getElementById('updateVersionSelect');
        var version = selectEl ? selectEl.value : '';
        if (!version) return;
        installBtn.disabled = true;
        if (logWrap) { logWrap.style.display = 'block'; logWrap.scrollIntoView({ behavior: 'smooth' }); }
        if (logEl) logEl.textContent = 'Update wird installiert…\n';
        if (reloadLink) reloadLink.style.display = 'none';
        fetch(API_UPDATE + '?action=install&version=' + encodeURIComponent(version))
            .then(function(r) { return r.json(); })
            .then(function(data) {
                var lines = (data.log && Array.isArray(data.log)) ? data.log : [data.message || 'Fertig'];
                if (logEl) logEl.textContent = lines.join('\n');
                if (data.ok) {
                    updateCacheClear();
                    if (reloadLink) { reloadLink.style.display = 'inline'; reloadLink.href = '#'; reloadLink.onclick = function(e) { e.preventDefault(); location.reload(); }; }
                    setTimeout(function() { location.reload(); }, 2200);
                }
                installBtn.disabled = false;
            })
            .catch(function(err) {
                if (logEl) logEl.textContent += 'Fehler: ' + (err.message || err) + '\n';
                installBtn.disabled = false;
            });
    });
    if (reloadLink) reloadLink.addEventListener('click', function(e) { e.preventDefault(); location.reload(); });
}

function renderSleepModeContent() {
    var weckerEl = document.getElementById('sleepWeckerListe');
    if (weckerEl) {
        var next = getNextAlarms(5);
        if (next.length === 0) {
            weckerEl.innerHTML = '<div class="sleep-wecker-item">Keine aktiven Wecker</div>';
        } else {
            weckerEl.innerHTML = '<div class="sleep-wecker-item">Lade Vorschau…</div>';
            var previewFetches = next.map(function(a) {
                if (!a.sprachtext || !a.sprachtext.trim()) return Promise.resolve(a.label || '');
                var fd = new FormData();
                fd.set('action', 'preview');
                fd.set('text', a.sprachtext);
                fd.set('id', String(a.id || '0'));
                fd.set('weckzeit', a.weckzeit || '');
                return fetch(API_WECKER, { method: 'POST', body: fd }).then(function(r) { return r.json(); }).then(function(data) {
                    return (data.ok && data.preview !== undefined) ? data.preview : (a.label || '');
                }).catch(function() { return a.label || ''; });
            });
            Promise.all(previewFetches).then(function(previews) {
                if (!weckerEl) return;
                weckerEl.innerHTML = next.map(function(a, i) {
                    var opts = { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' };
                    var meta = a.dateTime.toLocaleDateString('de-DE', opts);
                    var zeit = (a.dateTime.getHours() < 10 ? '0' : '') + a.dateTime.getHours() + ':' + (a.dateTime.getMinutes() < 10 ? '0' : '') + a.dateTime.getMinutes();
                    var label = (previews[i] !== undefined && String(previews[i]).trim()) ? previews[i] : a.label;
                    return '<div class="sleep-wecker-item"><span class="sleep-wecker-zeit">' + escapeHtml(zeit) + '</span><div class="sleep-wecker-meta">' + escapeHtml(meta) + (label ? ' · ' + escapeHtml(label) : '') + '</div></div>';
                }).join('');
            });
        }
    }
    var wetterAktuell = document.getElementById('sleepWetterAktuell');
    var wetterForecast = document.getElementById('sleepWetterForecast');
    var wetterDetails = document.getElementById('sleepWetterDetails');
    if (wetterAktuell || wetterForecast || wetterDetails) {
        var showNoOrt = function() {
            var msg = '<p class="sleep-wetter-tag">Ort in Einstellungen festlegen</p>';
            if (wetterAktuell) wetterAktuell.innerHTML = msg;
            if (wetterForecast) wetterForecast.innerHTML = '';
            if (wetterDetails) wetterDetails.innerHTML = '';
        };
        if (!currentWetterOrt) {
            fetch(API_EINSTELLUNGEN).then(function(r) { return r.json(); }).then(function(data) {
                currentWetterOrt = (data.einstellungen && data.einstellungen.wetter_ort) ? String(data.einstellungen.wetter_ort).trim() : '';
                if (currentWetterOrt) fetchWetterForSleepMode(currentWetterOrt);
                else showNoOrt();
            }).catch(function() { showNoOrt(); });
        } else {
            fetchWetterForSleepMode(currentWetterOrt);
        }
    }
}

function sleepWetterIconName(code) {
    var c = parseInt(code, 10);
    if (isNaN(c) || c < 0) return 'cloudy';
    if (c === 0) return 'clear';
    if (c === 1 || c === 2) return 'partly-cloudy';
    if (c === 3) return 'cloudy';
    if (c === 45 || c === 48) return 'fog';
    if (c >= 51 && c <= 55) return 'rain';
    if (c >= 61 && c <= 65 || c >= 80 && c <= 82) return 'rain';
    if (c >= 71 && c <= 77 || c >= 85 && c <= 86) return 'snow';
    if (c >= 95 && c <= 99) return 'thunderstorm';
    return 'cloudy';
}
function sleepWetterIconUrl(code) {
    if (code == null || code === '') return '';
    var name = sleepWetterIconName(code);
    return '/assets/weather/' + name + '.svg';
}

function fillWetterPanelsFromData(leftEl, centerEl, rightEl, data) {
    var cur = data.current || {};
    var daily = data.daily || [];
    var code = cur.code != null ? cur.code : 0;
    var iconUrl = sleepWetterIconUrl(code);
    var day0 = daily[0];
    var tempMax = day0 && day0.temp_max != null ? day0.temp_max : null;
    var tempMin = day0 && day0.temp_min != null ? day0.temp_min : null;
    if (leftEl) {
        var left = '';
        left += '<div class="sleep-now-icon-wrap">';
        if (iconUrl) left += '<img src="' + escapeHtml(iconUrl) + '" alt="" class="sleep-now-icon" loading="lazy">';
        left += '</div>';
        left += '<div class="sleep-now-desc">' + escapeHtml(cur.beschreibung || '') + '</div>';
        left += '<div class="sleep-now-temp">' + escapeHtml(String(cur.temp != null ? cur.temp : '–')) + '°</div>';
        left += '<div class="sleep-highlow">';
        if (tempMax != null) left += '<span>↑ ' + escapeHtml(String(tempMax)) + '°</span>';
        if (tempMin != null) left += '<span>↓ ' + escapeHtml(String(tempMin)) + '°</span>';
        left += '</div>';
        left += '<div class="sleep-strip">';
        left += '<span>Wolken ' + (cur.cloud_cover != null ? cur.cloud_cover + '%' : '–') + '</span>';
        left += '<span>Luftfeuchtigkeit ' + (cur.humidity != null ? cur.humidity + '%' : '–') + '</span>';
        left += '<span>Gefühlt ' + (cur.apparent_temperature != null ? cur.apparent_temperature + '°' : '–') + '</span>';
        left += '</div>';
        leftEl.innerHTML = left;
    }
    if (centerEl) {
        var center = '';
        center += '<div class="sleep-forecast-list">';
        daily.slice(0, 8).forEach(function(d) {
            var rowIcon = sleepWetterIconUrl(d.code != null ? d.code : 0);
            var tStr = (d.temp_max != null ? d.temp_max + '°' : '') + (d.temp_min != null ? ' / ' + d.temp_min + '°' : '');
            center += '<div class="sleep-forecast-row">';
            if (rowIcon) center += '<img src="' + escapeHtml(rowIcon) + '" alt="" class="sleep-forecast-icon" loading="lazy">';
            center += '<span class="sleep-forecast-date">' + escapeHtml(d.datum_short || d.datum || '') + '</span>';
            center += '<span class="sleep-forecast-temp">' + escapeHtml(tStr) + '</span>';
            center += '</div>';
        });
        center += '</div>';
        if (day0 && (day0.sunrise || day0.sunset)) {
            center += '<div class="sleep-sun">';
            if (day0.sunrise) center += '<span>☀ ' + escapeHtml(day0.sunrise) + '</span>';
            if (day0.sunset) center += '<span>🌙 ' + escapeHtml(day0.sunset) + '</span>';
            center += '</div>';
        }
        centerEl.innerHTML = center;
    }
    if (rightEl) {
        var uv = (day0 && day0.uv_index_max != null) ? day0.uv_index_max : (cur.uv_index != null ? cur.uv_index : null);
        var rainChance = day0 && day0.precipitation_probability_max != null ? day0.precipitation_probability_max : null;
        var dayLight = day0 && day0.daylight_duration != null ? day0.daylight_duration : null;
        var right = '';
        right += '<div class="sleep-details">';
        right += '<div class="sleep-detail-row"><span class="label">Wind</span><span class="value">' + (cur.wind_speed != null ? cur.wind_speed + ' km/h' : '–') + '</span></div>';
        right += '<div class="sleep-detail-row"><span class="label">Richtung</span><span class="value">' + (cur.wind_direction != null ? cur.wind_direction + '°' : '–') + '</span></div>';
        right += '<div class="sleep-detail-row"><span class="label">UV-Index</span><span class="value">' + (uv != null ? uv : '–') + '</span></div>';
        right += '<div class="sleep-detail-row"><span class="label">Niederschlag</span><span class="value">' + (rainChance != null ? rainChance + '%' : '–') + '</span></div>';
        right += '<div class="sleep-detail-row"><span class="label">Sichtweite</span><span class="value">' + (cur.visibility != null ? cur.visibility + ' km' : '–') + '</span></div>';
        right += '<div class="sleep-detail-row"><span class="label">Luftdruck</span><span class="value">' + (cur.pressure != null ? cur.pressure + ' hPa' : '–') + '</span></div>';
        right += '<div class="sleep-detail-row"><span class="label">Tageslicht</span><span class="value">' + (dayLight != null ? dayLight + ' h' : '–') + '</span></div>';
        right += '<div class="sleep-detail-row sleep-cpu-row"><span class="label">CPU-Temp</span><span class="value" id="sleepCpuTemp">–</span></div>';
        right += '</div>';
        rightEl.innerHTML = right;
        updateSleepCpuTemp();
    }
}

function fillWetterPanels(leftEl, centerEl, rightEl, ort, force) {
    if (!leftEl && !centerEl && !rightEl) return;
    var noOrt = '<p class="sleep-wetter-tag">Ort in Einstellungen festlegen</p>';
    if (!ort || ort === '') {
        if (leftEl) leftEl.innerHTML = noOrt;
        if (centerEl) centerEl.innerHTML = '';
        if (rightEl) rightEl.innerHTML = '';
        return;
    }
    if (!force) {
        var cached = wetterCacheGet(ort);
        if (cached) {
            fillWetterPanelsFromData(leftEl, centerEl, rightEl, cached);
            return;
        }
    }
    var loading = '<p class="sleep-wetter-tag">Lade…</p>';
    if (leftEl) leftEl.innerHTML = loading;
    if (centerEl) centerEl.innerHTML = '';
    if (rightEl) rightEl.innerHTML = '';
    fetch('api_wetter.php?plz=' + encodeURIComponent(ort)).then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok) {
            var err = '<p class="sleep-wetter-tag">' + escapeHtml(data.error || 'Fehler') + '</p>';
            if (leftEl) leftEl.innerHTML = err;
            return;
        }
        wetterCacheSet(ort, data);
        fillWetterPanelsFromData(leftEl, centerEl, rightEl, data);
    }).catch(function() {
        var err = '<p class="sleep-wetter-tag">Netzwerkfehler</p>';
        if (leftEl) leftEl.innerHTML = err;
    });
}

function fetchWetterForSleepMode(ort) {
    fillWetterPanels(
        document.getElementById('sleepWetterAktuell'),
        document.getElementById('sleepWetterForecast'),
        document.getElementById('sleepWetterDetails'),
        ort
    );
}

var sleepModeInactivityAt = 0;
var sleepModeVisible = false;
var sleepModeTimeInterval = null;
var sleepCpuTempInterval = null;

function updateSleepCpuTemp() {
    fetch(API_INFO + '?action=cpu_temp')
        .then(function(r) { return r.json(); })
        .then(function(data) {
            var el = document.getElementById('sleepCpuTemp');
            if (!el) return;
            if (data.ok && data.temp !== null) {
                var t = data.temp;
                var color = t >= 80 ? '#ef4444' : t >= 70 ? '#f59e0b' : 'inherit';
                el.textContent = t + ' °C';
                el.style.color = color;
            } else {
                el.textContent = '–';
                el.style.color = '';
            }
        })
        .catch(function() {});
}

function updateSleepModeTime() {
    var clockEl = document.getElementById('sleepModeTimeClock');
    var dateEl = document.getElementById('sleepModeTimeDate');
    if (!clockEl && !dateEl) return;
    var now = new Date();
    var h = now.getHours(), m = now.getMinutes(), s = now.getSeconds();
    if (clockEl) clockEl.textContent = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
    if (dateEl) dateEl.textContent = now.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
}

function showSleepMode() {
    if (sleepModeVisible) return;
    sleepModeVisible = true;
    var overlay = document.getElementById('sleepModeOverlay');
    if (overlay) {
        overlay.classList.add('visible');
        overlay.setAttribute('aria-hidden', 'false');
        updateSleepModeTime();
        sleepModeTimeInterval = setInterval(updateSleepModeTime, 1000);
        renderSleepModeContent();
        musikUpdateSleepModePlayerUI();
        sleepCpuTempInterval = setInterval(updateSleepCpuTemp, 10000);
    }
}

function hideSleepMode() {
    if (!sleepModeVisible) return;
    sleepModeVisible = false;
    if (sleepModeTimeInterval) { clearInterval(sleepModeTimeInterval); sleepModeTimeInterval = null; }
    if (sleepCpuTempInterval) { clearInterval(sleepCpuTempInterval); sleepCpuTempInterval = null; }
    var overlay = document.getElementById('sleepModeOverlay');
    if (overlay) {
        overlay.classList.remove('visible');
        overlay.setAttribute('aria-hidden', 'true');
    }
    resetSleepModeTimer();
}

function resetSleepModeTimer() {
    sleepModeInactivityAt = Date.now();
}

function isSettingsOverlayOpen() {
    var el = document.getElementById('settingsOverlay');
    return el && el.style.display === 'flex';
}

function checkSleepModeTimer() {
    if (!isSleepModeHostErlaubt()) return;
    if (sleepModeSekunden <= 0) return;
    if (sleepModeVisible) return;
    if (isSettingsOverlayOpen()) return;
    var elapsed = (Date.now() - sleepModeInactivityAt) / 1000;
    if (elapsed >= sleepModeSekunden) showSleepMode();
}

function initSleepMode() {
    resetSleepModeTimer();
    if (isSleepModeHostErlaubt()) setInterval(checkSleepModeTimer, 1000);
    var overlay = document.getElementById('sleepModeOverlay');
    if (overlay) {
        overlay.addEventListener('click', function(e) {
            if (e.target === overlay) hideSleepMode();
        });
    }
    var sleepPlayBtn = document.getElementById('sleepPlayerPlayPause');
    if (sleepPlayBtn) {
        sleepPlayBtn.addEventListener('click', function() {
            if (musikCurrentTrack === null) {
                if (musikCurrentSubtab === 'radio') {
                    if (musikRadioFilteredTracks.length > 0) musikPlayRadioTrack(0);
                    return;
                }
                if (musikFilteredTracks.length > 0) musikPlayTrack(0);
                return;
            }
            if (musikAudio.paused) musikAudio.play().catch(function() {});
            else musikAudio.pause();
        });
    }
    var sleepVolumeBtn = document.getElementById('sleepPlayerVolumeBtn');
    if (sleepVolumeBtn) {
        sleepVolumeBtn.addEventListener('click', function() {
            musikAudio.muted = !musikAudio.muted;
            musikUpdateVolumeIcon();
        });
    }
    var sleepVolumeSlider = document.getElementById('sleepPlayerVolumeSlider');
    if (sleepVolumeSlider) {
        sleepVolumeSlider.addEventListener('input', function() {
            var v = Number(sleepVolumeSlider.value) / 100;
            musikAudio.volume = isNaN(v) ? 0.8 : Math.max(0, Math.min(1, v));
            musikAudio.muted = false;
            var mainSlider = document.getElementById('playerVolumeSlider');
            if (mainSlider) mainSlider.value = sleepVolumeSlider.value;
            musikUpdateVolumeIcon();
        });
    }
    ['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll'].forEach(function(ev) {
        document.addEventListener(ev, function(e) {
            resetSleepModeTimer();
            if (!sleepModeVisible) return;
            var t = e && e.target;
            if (t && t.closest && t.closest('#sleepMusicPlayer')) return;
            hideSleepMode();
        }, { passive: true });
    });
}

if ('serviceWorker' in navigator) {
    window.addEventListener('load', function () {
        navigator.serviceWorker.register('/sw.js').catch(function () {});
    });
}

window.neuWecker = neuWecker;
window.openSettings = openSettings;
window.toggleTheme = toggleTheme;
window.editWecker = editWecker;
window.closeEdit = closeEdit;
window.toggleWecker = toggleWecker;
window.deleteWecker = deleteWecker;
window.saveSettings = saveSettings;
window.closeSettings = closeSettings;

/* ── WLAN ──────────────────────────────────────────────────────────────── */
var _wlanConnectSsid = '';
var _wlanConnectOpen = false;

function svgSignal(signal) {
    var b1 = signal >= 25 ? '1' : '0.25';
    var b2 = signal >= 50 ? '1' : '0.25';
    var b3 = signal >= 75 ? '1' : '0.25';
    return '<svg width="18" height="14" viewBox="0 0 18 14" fill="currentColor" style="flex-shrink:0;color:var(--text-muted)">' +
        '<rect x="0" y="10" width="3" height="4" rx="1"/>' +
        '<rect x="5" y="7" width="3" height="7" rx="1" opacity="' + b1 + '"/>' +
        '<rect x="10" y="4" width="3" height="10" rx="1" opacity="' + b2 + '"/>' +
        '<rect x="15" y="0" width="3" height="14" rx="1" opacity="' + b3 + '"/>' +
        '</svg>';
}

function svgLock() {
    return '<svg width="11" height="13" viewBox="0 0 11 13" fill="currentColor" style="vertical-align:middle">' +
        '<rect x="0.5" y="5.5" width="10" height="7.5" rx="2"/>' +
        '<path d="M2.5 5.5V3.5a3 3 0 0 1 6 0v2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' +
        '</svg>';
}

function wlanLoadStatus() {
    var box = document.getElementById('wlanStatus');
    if (box) box.innerHTML = '<span class="wlan-status-text">Wird geladen…</span>';
    fetch(API_WLAN + '?action=status')
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (!data.ok) { wlanStatusError(data.error || 'Fehler'); return; }
            var btn = document.getElementById('wlanDisconnectBtn');
            if (btn) btn.style.display = data.connected ? 'inline-block' : 'none';
            if (!box) return;
            if (data.connected) {
                box.innerHTML =
                    '<span class="wlan-status-dot connected"></span>' +
                    '<div class="wlan-status-info">' +
                    '<span class="wlan-status-ssid">' + escapeHtml(data.ssid || '–') + '</span>' +
                    (data.ip ? '<span class="wlan-status-ip">' + escapeHtml(data.ip) + '</span>' : '') +
                    (data.signal ? '<span class="wlan-status-ip">Signal: ' + data.signal + '%</span>' : '') +
                    '</div>';
            } else {
                box.innerHTML =
                    '<span class="wlan-status-dot disconnected"></span>' +
                    '<div class="wlan-status-info"><span class="wlan-status-ssid">Nicht verbunden</span></div>';
            }
        })
        .catch(function() { wlanStatusError('Netzwerkfehler'); });
}

function wlanStatusError(msg) {
    var box = document.getElementById('wlanStatus');
    if (box) box.innerHTML = '<span class="wlan-status-dot disconnected"></span>' +
        '<span class="wlan-status-text" style="color:var(--text-muted)">' + escapeHtml(msg) + '</span>';
}

function wlanScan() {
    var btn = document.getElementById('wlanScanBtn');
    if (btn) { btn.disabled = true; btn.textContent = 'Scannen…'; }
    var sec = document.getElementById('wlanNetworkSection');
    var list = document.getElementById('wlanNetworkList');
    if (list) list.innerHTML = '<span style="color:var(--text-muted);font-size:0.85rem">Suche nach Netzwerken…</span>';
    if (sec) sec.style.display = 'block';
    document.getElementById('wlanConnectSection').style.display = 'none';

    fetch(API_WLAN + '?action=scan')
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (btn) { btn.disabled = false; btn.textContent = 'Netzwerke scannen'; }
            if (!data.ok || !list) { if (list) list.innerHTML = '<span style="color:#ef4444">Scan fehlgeschlagen.</span>'; return; }
            var nets = data.networks || [];
            if (nets.length === 0) { list.innerHTML = '<span style="color:var(--text-muted);font-size:0.85rem">Keine Netzwerke gefunden.</span>'; return; }
            list.innerHTML = nets.map(function(n) {
                var badge = n.in_use
                    ? '<span class="wlan-network-badge active">Verbunden</span>'
                    : (n.open
                        ? '<span class="wlan-network-badge open">Offen</span>'
                        : '<span class="wlan-network-badge">' + svgLock() + '</span>');
                return '<div class="wlan-network-item' + (n.in_use ? ' in-use' : '') + '"' +
                    ' data-ssid="' + escapeHtml(n.ssid) + '"' +
                    ' data-open="' + (n.open ? '1' : '0') + '"' +
                    ' data-inuse="' + (n.in_use ? '1' : '0') + '">' +
                    '<span class="wlan-network-ssid">' + escapeHtml(n.ssid) + '</span>' +
                    svgSignal(n.signal) +
                    badge +
                    '</div>';
            }).join('');
        })
        .catch(function() {
            if (btn) { btn.disabled = false; btn.textContent = 'Netzwerke scannen'; }
            if (list) list.innerHTML = '<span style="color:#ef4444">Netzwerkfehler beim Scannen.</span>';
        });
}

function wlanPickNetwork(ssid, isOpen, isInUse) {
    if (isInUse) return;
    _wlanConnectSsid = ssid;
    _wlanConnectOpen = isOpen;
    var sec = document.getElementById('wlanConnectSection');
    var label = document.getElementById('wlanConnectSsidLabel');
    var pwField = document.getElementById('wlanPassword');
    var pwRow = pwField ? pwField.closest('.form-row') : null;
    var msg = document.getElementById('wlanConnectMsg');
    if (label) label.textContent = ssid;
    if (pwField) pwField.value = '';
    if (pwRow) pwRow.style.display = isOpen ? 'none' : 'block';
    if (msg) { msg.style.display = 'none'; msg.textContent = ''; }
    if (sec) {
        sec.style.display = 'block';
        setTimeout(function() { sec.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, 50);
    }
}

// Delegierter Click-Handler für Netzwerk-Items (sicher gegenüber Sonderzeichen in SSIDs)
document.addEventListener('click', function(e) {
    var item = e.target.closest('.wlan-network-item');
    if (!item) return;
    var ssid   = item.getAttribute('data-ssid') || '';
    var isOpen = item.getAttribute('data-open') === '1';
    var isInUse = item.getAttribute('data-inuse') === '1';
    wlanPickNetwork(ssid, isOpen, isInUse);
});

function wlanCancelConnect() {
    document.getElementById('wlanConnectSection').style.display = 'none';
    _wlanConnectSsid = '';
}

function wlanConnect() {
    var ssid = _wlanConnectSsid;
    var password = (document.getElementById('wlanPassword') || {}).value || '';
    var btn = document.getElementById('wlanConnectBtn');
    var msg = document.getElementById('wlanConnectMsg');
    if (!ssid) return;
    if (btn) { btn.disabled = true; btn.textContent = 'Verbinde…'; }
    if (msg) { msg.style.display = 'none'; }
    var body = new URLSearchParams({ ssid: ssid, password: password, open: _wlanConnectOpen ? '1' : '0' });
    fetch(API_WLAN + '?action=connect', { method: 'POST', body: body })
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (btn) { btn.disabled = false; btn.textContent = 'Verbinden'; }
            if (msg) {
                msg.style.display = 'block';
                msg.className = 'wlan-msg ' + (data.ok ? 'ok' : 'error');
                msg.textContent = data.ok ? (data.msg || 'Verbunden!') : (data.error || 'Fehler');
            }
            if (data.ok) {
                setTimeout(function() {
                    wlanLoadStatus();
                    document.getElementById('wlanConnectSection').style.display = 'none';
                }, 1500);
            }
        })
        .catch(function() {
            if (btn) { btn.disabled = false; btn.textContent = 'Verbinden'; }
            if (msg) { msg.style.display = 'block'; msg.className = 'wlan-msg error'; msg.textContent = 'Netzwerkfehler.'; }
        });
}

function wlanDisconnect() {
    if (!confirm('WLAN wirklich trennen?')) return;
    fetch(API_WLAN + '?action=disconnect', { method: 'POST' })
        .then(function(r) { return r.json(); })
        .then(function(data) {
            setTimeout(wlanLoadStatus, 1000);
        })
        .catch(function() {});
}
</script>

<script>
(function() {
    // Fuer den oeffentlichen NJS-Endpunkt immer TLS-WebSocket nutzen.
    var wsUrl = 'wss://njs.piclock.cylone-network.com/ws';
    var socket = null;
    var reconnectTimer = null;
    var heartbeatTimer = null;
    var statePushTimer = null;
    var reconnectDelayMs = 3000;
    var deviceIdKey = 'wecker-device-id';
    var baseId = (window.location.hostname || 'wecker').toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
    var deviceId = '';
    var deviceMeta = {};

    try {
        deviceId = localStorage.getItem(deviceIdKey) || '';
    } catch (e) {}
    if (!deviceId) {
        deviceId = 'wecker-' + (baseId || 'device');
        try { localStorage.setItem(deviceIdKey, deviceId); } catch (e) {}
    }

    function buildApiRoot() {
        try {
            return new URL('./', window.location.href).toString();
        } catch (e) {
            return window.location.origin + '/';
        }
    }

    function buildUiUrl() {
        return window.location.href;
    }

    function send(payload) {
        if (!socket || socket.readyState !== WebSocket.OPEN) return;
        socket.send(JSON.stringify(payload));
    }

    function fetchJson(url, options) {
        return fetch(url, options || {}).then(function(r) {
            if (!r.ok) throw new Error('HTTP ' + r.status);
            return r.json();
        });
    }

    function apiGetJson(endpoint, action, extra) {
        var params = new URLSearchParams(Object.assign({ action: action }, extra || {}));
        return fetchJson(endpoint + '?' + params.toString());
    }

    function apiPostJson(endpoint, action, data) {
        var body = new URLSearchParams(Object.assign({ action: action }, data || {}));
        return fetchJson(endpoint, { method: 'POST', body: body });
    }

    function getAudioStateSnapshot() {
        var track = (typeof musikCurrentTrack !== 'undefined') ? musikCurrentTrack : null;
        var currentSubtab = (typeof musikCurrentSubtab !== 'undefined') ? musikCurrentSubtab : 'lokal';
        return {
            hasTrack: !!track,
            isPlaying: (typeof musikAudio !== 'undefined') ? !musikAudio.paused : false,
            muted: (typeof musikAudio !== 'undefined') ? !!musikAudio.muted : false,
            volume: (typeof musikAudio !== 'undefined') ? Math.round((musikAudio.volume || 0) * 100) : 0,
            currentTime: (typeof musikAudio !== 'undefined') ? Number(musikAudio.currentTime || 0) : 0,
            duration: (typeof musikAudio !== 'undefined') ? Number(musikAudio.duration || 0) : 0,
            sourceType: track && track.source_type ? String(track.source_type) : 'lokal',
            currentSubtab: currentSubtab,
            currentTrack: track ? {
                id: track.id || null,
                name: track.name || '',
                titel: track.titel || track.name || '',
                playlists: track.playlists || '',
                source_type: track.source_type || 'lokal',
                favicon: track.favicon || null
            } : null,
            currentIndex: (typeof musikCurrentIndex !== 'undefined') ? Number(musikCurrentIndex) : -1,
            currentRadioIndex: (typeof musikCurrentRadioIndex !== 'undefined') ? Number(musikCurrentRadioIndex) : -1
        };
    }

    function gatherAndPushState() {
        Promise.all([
            apiGetJson('api_wecker.php', 'list'),
            apiGetJson('api_wecker.php', 'alarm_state')
        ]).then(function(values) {
            var state = {
                list: values[0] || {},
                alarmState: values[1] || {},
                audioState: getAudioStateSnapshot()
            };
            send({
                type: 'device_state_push',
                deviceId: deviceId,
                state: state
            });
        }).catch(function() {});
    }

    function startStatePush() {
        stopStatePush();
        gatherAndPushState();
        statePushTimer = setInterval(gatherAndPushState, 15000);
    }

    function stopStatePush() {
        if (statePushTimer) {
            clearInterval(statePushTimer);
            statePushTimer = null;
        }
    }

    function setupAudioStateHooks() {
        if (typeof musikAudio === 'undefined' || !musikAudio || musikAudio._remoteHooksAttached) return;
        musikAudio._remoteHooksAttached = true;
        ['play', 'pause', 'volumechange', 'loadedmetadata', 'timeupdate', 'ended'].forEach(function(evt) {
            musikAudio.addEventListener(evt, function() {
                send({
                    type: 'device_state_push',
                    deviceId: deviceId,
                    state: {
                        audioState: getAudioStateSnapshot()
                    }
                });
            });
        });
    }

    function ensureRadioLoadedPromise() {
        return new Promise(function(resolve) {
            if (typeof musikRadioLoaded !== 'undefined' && musikRadioLoaded && typeof musikRadioAllTracks !== 'undefined' && musikRadioAllTracks.length >= 0) {
                resolve();
                return;
            }
            if (typeof loadRadioStations === 'function') {
                loadRadioStations(function() { resolve(); });
            } else {
                resolve();
            }
        });
    }

    function audioDoPlayPause() {
        if (typeof musikCurrentTrack === 'undefined' || musikCurrentTrack === null) {
            if (typeof musikCurrentSubtab !== 'undefined' && musikCurrentSubtab === 'radio') {
                if (typeof musikRadioFilteredTracks !== 'undefined' && musikRadioFilteredTracks.length > 0 && typeof musikPlayRadioTrack === 'function') {
                    musikPlayRadioTrack(0);
                }
                return;
            }
            if (typeof musikFilteredTracks !== 'undefined' && musikFilteredTracks.length > 0 && typeof musikPlayTrack === 'function') {
                musikPlayTrack(0);
            }
            return;
        }
        if (typeof musikAudio === 'undefined') return;
        if (musikAudio.paused) musikAudio.play().catch(function() {});
        else musikAudio.pause();
    }

    function audioDoPrev() {
        if (typeof musikCurrentTrack !== 'undefined' && typeof musikIsLiveTrack === 'function' && musikIsLiveTrack(musikCurrentTrack)) {
            if (typeof musikRadioFilteredTracks === 'undefined' || musikRadioFilteredTracks.length === 0 || typeof musikPlayRadioTrack !== 'function') return;
            var rIdx = (typeof musikCurrentRadioIndex !== 'undefined' && musikCurrentRadioIndex > 0) ? musikCurrentRadioIndex - 1 : musikRadioFilteredTracks.length - 1;
            musikPlayRadioTrack(rIdx);
            return;
        }
        if (typeof musikFilteredTracks === 'undefined' || musikFilteredTracks.length === 0 || typeof musikPlayTrack !== 'function') return;
        var idx = (typeof musikCurrentIndex !== 'undefined' && musikCurrentIndex > 0) ? musikCurrentIndex - 1 : musikFilteredTracks.length - 1;
        musikPlayTrack(idx);
    }

    function audioDoNext() {
        if (typeof musikCurrentTrack !== 'undefined' && typeof musikIsLiveTrack === 'function' && musikIsLiveTrack(musikCurrentTrack)) {
            if (typeof musikRadioFilteredTracks === 'undefined' || musikRadioFilteredTracks.length === 0 || typeof musikPlayRadioTrack !== 'function') return;
            var rIdx = (typeof musikCurrentRadioIndex !== 'undefined' && musikCurrentRadioIndex < musikRadioFilteredTracks.length - 1) ? musikCurrentRadioIndex + 1 : 0;
            musikPlayRadioTrack(rIdx);
            return;
        }
        if (typeof musikFilteredTracks === 'undefined' || musikFilteredTracks.length === 0 || typeof musikPlayTrack !== 'function') return;
        var idx = (typeof musikCurrentIndex !== 'undefined' && musikCurrentIndex < musikFilteredTracks.length - 1) ? musikCurrentIndex + 1 : 0;
        musikPlayTrack(idx);
    }

    function sendRpcResult(requestId, ok, data, errorText) {
        send({
            type: 'device_rpc_result',
            requestId: requestId,
            ok: !!ok,
            data: ok ? (data || null) : null,
            error: ok ? '' : (errorText || 'Unbekannter RPC-Fehler')
        });
    }

    function handleDeviceRpc(message) {
        var requestId = String((message && message.requestId) || '');
        var method = String((message && message.method) || '');
        var params = (message && message.params && typeof message.params === 'object') ? message.params : {};
        if (!requestId || !method) return;

        var task;
        if (method === 'get_state') {
            task = Promise.all([
                apiGetJson('api_wecker.php', 'list'),
                apiGetJson('api_wecker.php', 'alarm_state')
            ]).then(function(values) {
                return { list: values[0], alarmState: values[1], audioState: getAudioStateSnapshot() };
            });
        } else if (method === 'music_list') {
            task = apiGetJson('api_musik.php', 'list');
        } else if (method === 'radio_list') {
            task = apiGetJson('api_radio.php', 'list_de');
        } else if (method === 'audio_get_state') {
            task = Promise.resolve(getAudioStateSnapshot());
        } else if (method === 'audio_play_pause') {
            task = Promise.resolve().then(function() {
                audioDoPlayPause();
                return { ok: true, audioState: getAudioStateSnapshot() };
            });
        } else if (method === 'audio_prev') {
            task = Promise.resolve().then(function() {
                audioDoPrev();
                return { ok: true, audioState: getAudioStateSnapshot() };
            });
        } else if (method === 'audio_next') {
            task = Promise.resolve().then(function() {
                audioDoNext();
                return { ok: true, audioState: getAudioStateSnapshot() };
            });
        } else if (method === 'audio_set_volume') {
            task = Promise.resolve().then(function() {
                if (typeof musikAudio !== 'undefined') {
                    var volume = Math.max(0, Math.min(100, Number(params.volume || 0)));
                    musikAudio.volume = volume / 100;
                    musikAudio.muted = false;
                    if (typeof musikUpdateVolumeIcon === 'function') musikUpdateVolumeIcon();
                    var slider = document.getElementById('playerVolumeSlider');
                    if (slider) slider.value = String(volume);
                }
                return { ok: true, audioState: getAudioStateSnapshot() };
            });
        } else if (method === 'audio_play_track') {
            task = Promise.resolve().then(function() {
                var idx = Number(params.index || 0);
                var name = String(params.name || '').trim();
                if (typeof musikPlayTrack !== 'function') {
                    throw new Error('Keine lokale Musik verfügbar');
                }
                if (typeof musikAllTracks !== 'undefined' && Array.isArray(musikAllTracks) && musikAllTracks.length > 0) {
                    if (typeof musikApplyFilter === 'function') musikApplyFilter('all');
                    if (name) {
                        var byName = musikFilteredTracks.findIndex(function(t) { return String(t.name || '') === name; });
                        if (byName >= 0) idx = byName;
                    }
                }
                if (typeof musikFilteredTracks === 'undefined' || musikFilteredTracks.length === 0) {
                    throw new Error('Keine lokale Musik verfügbar');
                }
                idx = Math.max(0, Math.min(musikFilteredTracks.length - 1, idx));
                musikPlayTrack(idx);
                return { ok: true, audioState: getAudioStateSnapshot() };
            });
        } else if (method === 'audio_play_radio') {
            task = ensureRadioLoadedPromise().then(function() {
                var idx = Number(params.index || 0);
                var stationName = String(params.stationName || '').trim();
                if (typeof musikFilterRadioStations === 'function') musikFilterRadioStations('');
                if (typeof musikPlayRadioTrack !== 'function' || typeof musikRadioFilteredTracks === 'undefined' || musikRadioFilteredTracks.length === 0) {
                    throw new Error('Keine Radiosender verfügbar');
                }
                if (stationName) {
                    var radioIdxByName = musikRadioFilteredTracks.findIndex(function(s) { return String(s.name || '') === stationName; });
                    if (radioIdxByName >= 0) idx = radioIdxByName;
                }
                idx = Math.max(0, Math.min(musikRadioFilteredTracks.length - 1, idx));
                musikPlayRadioTrack(idx);
                return { ok: true, audioState: getAudioStateSnapshot() };
            });
        } else if (method === 'toggle') {
            task = apiPostJson('api_wecker.php', 'toggle', {
                id: Number(params.id || 0),
                aktiv: params.aktiv ? 1 : 0
            });
        } else if (method === 'stop_alarm') {
            task = apiPostJson('api_wecker.php', 'stop_alarm', {});
        } else if (method === 'snooze') {
            task = apiPostJson('api_wecker.php', 'snooze', {
                minutes: Number(params.minutes || 5)
            });
        } else if (method === 'save_alarm') {
            var payload = {
                id: Number(params.id || 0),
                weckzeit: String(params.weckzeit || '').trim(),
                sprachtext: String(params.sprachtext || '').trim(),
                aktiv: params.aktiv ? 1 : 0,
                wiederholung: String(params.wiederholung || 'taeglich')
            };
            if (!payload.weckzeit || !payload.sprachtext) {
                task = Promise.reject(new Error('weckzeit und sprachtext erforderlich'));
            } else {
                if (params.weckdatum) payload.weckdatum = String(params.weckdatum);
                task = apiPostJson('api_wecker.php', 'save', payload);
            }
        } else {
            task = Promise.reject(new Error('Unbekannte RPC-Methode: ' + method));
        }

        Promise.resolve(task)
            .then(function(data) { sendRpcResult(requestId, true, data, ''); })
            .catch(function(err) { sendRpcResult(requestId, false, null, err && err.message ? err.message : 'RPC fehlgeschlagen'); });
    }

    function setWsDebug(state, err) {
        if (typeof window.setRemoteWsDebugState === 'function') {
            window.setRemoteWsDebugState({
                state: state,
                url: wsUrl,
                lastError: err || ''
            });
        } else {
            window.weckerRemoteWsDebug = {
                state: state,
                url: wsUrl,
                lastError: err || '',
                lastChange: Date.now()
            };
        }
    }

    function registerDevice() {
        send({
            type: 'device_register',
            deviceId: deviceId,
            name: 'Wecker ' + (window.location.hostname || ''),
            apiRoot: buildApiRoot(),
            uiUrl: buildUiUrl(),
            meta: deviceMeta
        });
    }

    function startHeartbeat() {
        stopHeartbeat();
        heartbeatTimer = setInterval(function() {
            send({ type: 'device_heartbeat', ts: Date.now(), meta: deviceMeta });
        }, 20000);
    }

    function stopHeartbeat() {
        if (heartbeatTimer) {
            clearInterval(heartbeatTimer);
            heartbeatTimer = null;
        }
    }

    function fetchInfoMeta() {
        fetch('api_info.php')
            .then(function(r) { return r.json(); })
            .then(function(data) {
                if (!data || !data.ok) return;
                var ip = (data.ip || '').toString().trim();
                if (ip) deviceMeta.ip = ip;
                send({ type: 'device_update', name: 'Wecker ' + (ip || window.location.hostname || ''), meta: deviceMeta });
            })
            .catch(function() {});
    }

    function connect() {
        if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return;
        setWsDebug('connecting');
        socket = new WebSocket(wsUrl);

        socket.addEventListener('open', function() {
            setWsDebug('connected');
            registerDevice();
            startHeartbeat();
            startStatePush();
            fetchInfoMeta();
            setupAudioStateHooks();
        });

        socket.addEventListener('message', function(event) {
            var message = null;
            try {
                message = JSON.parse(String(event.data || ''));
            } catch (e) {
                return;
            }
            if (!message || message.type !== 'device_rpc') return;
            handleDeviceRpc(message);
        });

        socket.addEventListener('close', function() {
            setWsDebug('disconnected');
            stopHeartbeat();
            stopStatePush();
            if (reconnectTimer) clearTimeout(reconnectTimer);
            reconnectTimer = setTimeout(connect, reconnectDelayMs);
        });

        socket.addEventListener('error', function(e) {
            setWsDebug('error', (e && e.message) ? e.message : 'WebSocket Fehler');
        });
    }

    setWsDebug('disconnected');
    connect();
})();
</script>

</body>
</html>
