(() => { const toast = document.querySelector('[data-toast]'); if (toast) { const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const region = toast.closest('.toast-region'); const duration = toast.classList.contains('toast-error') ? 6500 : 4800; let remaining = duration; let startedAt = 0; let dismissTimer; let timerRunning = false; const dismiss = () => { if (!toast.isConnected || toast.classList.contains('is-leaving')) return; window.clearTimeout(dismissTimer); timerRunning = false; toast.classList.add('is-leaving'); if (reducedMotion) { region?.remove(); return; } window.setTimeout(() => region?.remove(), 220); }; const resume = () => { if (timerRunning || toast.classList.contains('is-leaving')) return; toast.classList.remove('is-paused'); startedAt = performance.now(); timerRunning = true; dismissTimer = window.setTimeout(dismiss, remaining); }; const pause = () => { if (!timerRunning || toast.classList.contains('is-leaving')) return; remaining = Math.max(0, remaining - (performance.now() - startedAt)); window.clearTimeout(dismissTimer); timerRunning = false; toast.classList.add('is-paused'); }; toast.querySelector('[data-toast-close]')?.addEventListener('click', dismiss); toast.addEventListener('pointerenter', pause); toast.addEventListener('pointerleave', () => { if (!toast.contains(document.activeElement)) resume(); }); toast.addEventListener('focusin', pause); toast.addEventListener('focusout', () => { window.requestAnimationFrame(() => { if (!toast.matches(':hover') && !toast.contains(document.activeElement)) resume(); }); }); resume(); } const currentURL = new URL(window.location.href); if (currentURL.searchParams.has('notice')) { currentURL.searchParams.delete('notice'); currentURL.searchParams.delete('notice_kind'); const cleanLocation = `${currentURL.pathname}${currentURL.search}${currentURL.hash}`; window.history.replaceState(window.history.state, '', cleanLocation); } const settingsForm = document.querySelector('[data-settings-form]'); if (settingsForm) { const updaterToggle = settingsForm.querySelector('[data-updater-toggle]'); const ipv6Toggle = settingsForm.querySelector('[data-ipv6-toggle]'); const ipv6Settings = settingsForm.querySelector('[data-ipv6-settings]'); const syncToggleCard = (input) => { if (!input) return; const card = input.closest('.setting-switch-card'); card?.classList.toggle('is-enabled', input.checked); const label = input.closest('.setting-switch')?.querySelector('b'); if (label) label.textContent = input.checked ? 'Aktiv' : 'Inaktiv'; }; const syncIPv6Settings = () => { if (!ipv6Toggle || !ipv6Settings) return; ipv6Settings.classList.toggle('is-disabled', !ipv6Toggle.checked); ipv6Settings.setAttribute('aria-disabled', String(!ipv6Toggle.checked)); const textarea = ipv6Settings.querySelector('textarea'); if (textarea) textarea.required = ipv6Toggle.checked; }; updaterToggle?.addEventListener('change', () => syncToggleCard(updaterToggle)); ipv6Toggle?.addEventListener('change', () => { syncToggleCard(ipv6Toggle); syncIPv6Settings(); }); syncToggleCard(updaterToggle); syncToggleCard(ipv6Toggle); syncIPv6Settings(); } for (const form of document.querySelectorAll('[data-target-form]')) { const active = form.querySelector('[data-target-active]'); const staticFields = form.querySelector('[data-static-ip-fields]'); const ipv4Toggle = form.querySelector('[name="update_ipv4"]'); const ipv6Toggle = form.querySelector('[name="update_ipv6"]'); const staticIPv4 = form.querySelector('[data-static-ipv4]'); const staticIPv6 = form.querySelector('[data-static-ipv6]'); const requirement = form.querySelector('[data-target-requirement]'); const submit = form.querySelector('button[type="submit"]'); const ipv6Enabled = form.dataset.ipv6Enabled !== 'false'; const ipv4Selected = () => ipv4Toggle ? ipv4Toggle.checked : form.dataset.updateIpv4 === 'true'; const ipv6Selected = () => ipv6Enabled && (ipv6Toggle ? ipv6Toggle.checked : form.dataset.updateIpv6 === 'true'); const syncStaticFields = () => { if (!active || !staticFields) return; const useIPv4 = ipv4Selected(); const useIPv6 = ipv6Selected(); const hasFamily = useIPv4 || useIPv6; if (active.checked && !hasFamily) active.checked = false; active.disabled = !hasFamily; active.closest('.switch')?.classList.toggle('switch-disabled', !hasFamily); const useStatic = !active.checked && hasFamily; staticFields.hidden = !useStatic; staticFields.setAttribute('aria-hidden', String(!useStatic)); if (staticIPv4) { staticIPv4.disabled = !useStatic || !useIPv4; staticIPv4.required = useStatic && useIPv4; } if (staticIPv6) { staticIPv6.disabled = !useStatic || !useIPv6; staticIPv6.required = useStatic && useIPv6; } const missingStaticIP = useStatic && ( (useIPv4 && !staticIPv4?.value.trim()) || (useIPv6 && !staticIPv6?.value.trim()) ); if (submit) submit.disabled = missingStaticIP; if (requirement) requirement.hidden = !missingStaticIP; }; active?.addEventListener('change', syncStaticFields); for (const familyToggle of [ipv4Toggle, ipv6Toggle]) { familyToggle?.addEventListener('change', () => { if (!familyToggle.checked && active) active.checked = false; syncStaticFields(); }); } staticIPv4?.addEventListener('input', syncStaticFields); staticIPv6?.addEventListener('input', syncStaticFields); syncStaticFields(); } for (const zone of document.querySelectorAll('[data-access-zone]')) { const access = zone.querySelector('[data-zone-access]'); const manage = zone.querySelector('[data-zone-manage]'); const create = zone.querySelector('[data-zone-create]'); access?.addEventListener('change', () => { if (!access.checked) { if (manage) manage.checked = false; if (create) create.checked = false; } }); manage?.addEventListener('change', () => { if (manage.checked && access) access.checked = true; if (!manage.checked && create) create.checked = false; }); create?.addEventListener('change', () => { if (create.checked) { if (access) access.checked = true; if (manage) manage.checked = true; } }); } for (const target of document.querySelectorAll('[data-access-target]')) { const access = target.querySelector('[data-target-access]'); const manage = target.querySelector('[data-target-manage]'); access?.addEventListener('change', () => { if (!access.checked && manage) manage.checked = false; }); manage?.addEventListener('change', () => { if (manage.checked && access) access.checked = true; }); } const copyButtons = document.querySelectorAll('[data-copy-target]'); for (const button of copyButtons) { button.addEventListener('click', async () => { const target = document.getElementById(button.dataset.copyTarget); if (!target) return; try { await navigator.clipboard.writeText(target.textContent); const original = button.textContent; button.textContent = 'Kopiert'; window.setTimeout(() => { button.textContent = original; }, 2000); } catch { button.textContent = 'Manuell kopieren'; } }); } const countdowns = [...document.querySelectorAll('[data-countdown]')]; if (countdowns.length === 0) return; let nextRun = Number(countdowns[0].dataset.countdown || 0); let running = countdowns[0].dataset.running === 'true'; let enabled = countdowns[0].dataset.enabled !== 'false'; let lastRefresh = 0; const formatRemaining = (seconds) => { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const rest = seconds % 60; if (hours > 0) return `${hours}:${String(minutes).padStart(2, '0')}:${String(rest).padStart(2, '0')}`; return `${String(minutes).padStart(2, '0')}:${String(rest).padStart(2, '0')}`; }; const refreshSchedule = async () => { const now = Date.now(); if (now - lastRefresh < 2500) return; lastRefresh = now; try { const response = await fetch('/api/v1/scheduler', {headers: {'Accept': 'application/json'}, cache: 'no-store'}); if (!response.ok) return; const state = await response.json(); nextRun = Number(state.next_run_unix || 0); running = Boolean(state.running); enabled = state.enabled !== false; } catch { // The visible countdown keeps its last known state when the status request fails. } }; const tick = () => { if (!enabled) { for (const node of countdowns) { node.textContent = 'Inaktiv'; node.setAttribute('aria-label', 'Automatischer Updater ist deaktiviert'); } return; } const now = Math.floor(Date.now() / 1000); const remaining = Math.max(0, nextRun - now); const label = running ? 'Update läuft' : (nextRun > 0 ? formatRemaining(remaining) : '--:--'); for (const node of countdowns) { node.textContent = label; node.setAttribute('aria-label', running ? 'Update läuft' : `Nächste Aktualisierung in ${remaining} Sekunden`); } if (running || nextRun === 0 || remaining === 0) refreshSchedule(); }; tick(); refreshSchedule(); window.setInterval(tick, 1000); window.setInterval(refreshSchedule, 30000); })();