CharGraph-FW/html/index.html
2026-01-25 20:44:07 +01:00

2624 lines
118 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CharGraph Konfiguration</title>
<style>
:root {
--bg-gradient-start: #667eea;
--bg-gradient-end: #764ba2;
--container-bg: white;
--text-primary: #333;
--text-secondary: #555;
--text-tertiary: #666;
--border-color: #ddd;
--hover-bg: #f5f5f5;
--info-bg: #e8eaf6;
--accent-color: #667eea;
--input-bg: white;
--shadow: rgba(0,0,0,0.3);
}
body.dark-mode {
--bg-gradient-start: #1a1a2e;
--bg-gradient-end: #16213e;
--container-bg: #0f3460;
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--text-tertiary: #999;
--border-color: #444;
--hover-bg: #1a4d7a;
--info-bg: #1a3a5a;
--accent-color: #667eea;
--input-bg: #1a1a2e;
--shadow: rgba(0,0,0,0.6);
}
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
transition: background 0.3s ease;
}
.container {
background: var(--container-bg);
padding: 30px;
border-radius: 15px;
box-shadow: 0 8px 20px var(--shadow);
position: relative;
transition: background 0.3s ease;
}
h1 {
color: var(--text-primary);
text-align: center;
margin-bottom: 30px;
transition: color 0.3s ease;
}
h2 {
color: var(--text-secondary);
font-size: 18px;
margin-top: 25px;
margin-bottom: 15px;
border-bottom: 2px solid var(--accent-color);
padding-bottom: 5px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
transition: color 0.3s ease;
}
h2:hover {
background: var(--hover-bg);
padding-left: 10px;
padding-right: 10px;
margin-left: -10px;
margin-right: -10px;
border-radius: 8px;
}
.theme-toggle {
position: absolute;
top: 15px;
right: 15px;
width: 40px;
height: 40px;
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
padding: 0;
}
.theme-toggle:hover {
transform: scale(1.15) rotate(20deg);
}
.theme-toggle svg {
width: 40px;
height: 40px;
fill: var(--text-primary);
stroke: var(--text-primary);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
transition: fill 0.3s ease, stroke 0.3s ease;
}
.section-toggle {
font-size: 20px;
transition: transform 0.3s ease;
}
.section-toggle.collapsed {
transform: rotate(-90deg);
}
.section-content {
max-height: 10000px;
overflow: hidden;
transition: max-height 0.3s ease, opacity 0.3s ease;
opacity: 1;
}
.section-content.collapsed {
max-height: 0;
opacity: 0;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: var(--text-secondary);
transition: color 0.3s ease;
}
input[type="color"] {
width: 100%;
height: 60px;
border: 2px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
}
input[type="range"] {
width: 100%;
height: 8px;
-webkit-appearance: none;
appearance: none;
background: var(--border-color);
border-radius: 5px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
background: var(--accent-color);
cursor: pointer;
border-radius: 50%;
}
input[type="datetime-local"], input[type="number"], input[type="text"], input[type="password"], select {
width: 100%;
padding: 12px;
border: 2px solid var(--border-color);
border-radius: 8px;
font-size: 16px;
box-sizing: border-box;
background: var(--input-bg);
color: var(--text-primary);
transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
textarea {
width: 100%;
font-family: 'Courier New', monospace;
font-size: 16px;
padding: 10px;
border: 2px solid var(--border-color);
border-radius: 8px;
resize: none;
letter-spacing: 2px;
line-height: 1.5;
background: var(--input-bg);
color: var(--text-primary);
box-sizing: border-box;
transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
body.dark-mode textarea {
background: linear-gradient(0deg, transparent 24px, #444 25px),
linear-gradient(90deg, transparent 24px, #444 25px),
var(--input-bg);
background-size: 25px 25px;
}
body:not(.dark-mode) textarea {
background: linear-gradient(0deg, transparent 24px, #e0e0e0 25px),
linear-gradient(90deg, transparent 24px, #e0e0e0 25px),
white;
background-size: 25px 25px;
}
.range-value {
text-align: center;
font-size: 24px;
font-weight: bold;
color: var(--accent-color);
margin-top: 10px;
}
.time-option, .test-option {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.test-option {
padding: 10px;
background: var(--hover-bg);
border-radius: 8px;
transition: background 0.3s ease;
}
.time-option input[type="radio"], .test-option input[type="radio"] {
margin-right: 10px;
width: 20px;
height: 20px;
}
.time-option label, .test-option label {
margin: 0;
font-weight: normal;
cursor: pointer;
}
.manual-time {
display: none;
margin-top: 15px;
padding: 15px;
background: var(--hover-bg);
border-radius: 8px;
transition: background 0.3s ease;
}
.test-modes {
display: flex;
flex-direction: column;
gap: 10px;
}
button {
width: 100%;
padding: 18px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
margin-top: 10px;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
button.secondary {
width: auto;
padding: 10px 20px;
font-size: 14px;
background: #999;
}
.info {
background: var(--info-bg);
padding: 20px;
border-radius: 8px;
margin-top: 25px;
border-left: 4px solid var(--accent-color);
transition: background 0.3s ease;
}
.info h3 {
margin-top: 0;
color: var(--text-primary);
transition: color 0.3s ease;
}
.time-display {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
.time-box {
flex: 1;
text-align: center;
}
.time-box strong {
display: block;
margin-bottom: 5px;
font-size: 14px;
}
.time-value {
font-size: 22px;
color: var(--accent-color);
font-weight: bold;
}
#driftInfo {
padding: 15px;
border-radius: 8px;
margin-top: 15px;
display: none;
}
.drift-value {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
.drift-status {
font-size: 14px;
color: var(--text-tertiary);
transition: color 0.3s ease;
}
.drift-good {
background: #c8e6c9;
border-left: 4px solid #4caf50;
}
.drift-warning {
background: #fff9c4;
border-left: 4px solid #ffc107;
}
.drift-bad {
background: #ffcdd2;
border-left: 4px solid #f44336;
}
.details {
margin-top: 15px;
font-size: 13px;
color: var(--text-tertiary);
padding-top: 15px;
border-top: 1px solid var(--border-color);
transition: color 0.3s ease, border-color 0.3s ease;
}
.details div {
margin: 5px 0;
}
.color-preview {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
.color-box {
width: 48%;
height: 40px;
border-radius: 5px;
border: 2px solid var(--border-color);
transition: border-color 0.3s ease;
}
.chargraph-icon {
display: inline-block;
width: 28px;
height: 28px;
margin-right: 8px;
vertical-align: middle;
}
.chargraph-icon.small {
width: 22px;
height: 22px;
margin-right: 6px;
}
.password-toggle-icon {
width: 24px;
height: 24px;
fill: none;
stroke: #999;
stroke-width: 2;
}
</style>
</head>
<body>
<!-- SVG Definitions -->
<svg style="display: none;">
<defs>
<!-- CharGraph Icon -->
<symbol id="icon-chargraph" viewBox="0 0 44 44">
<rect width="44" height="44" fill="#1a1a1a" rx="2"/>
<g fill="#fff">
<rect x="2" y="2" width="3" height="3"/>
<rect x="6" y="2" width="3" height="3"/>
<rect x="14" y="2" width="3" height="3"/>
<rect x="18" y="2" width="3" height="3"/>
<rect x="22" y="2" width="3" height="3"/>
<rect x="2" y="34" width="3" height="3"/>
<rect x="6" y="34" width="3" height="3"/>
<rect x="10" y="34" width="3" height="3"/>
<rect x="14" y="34" width="3" height="3"/>
<rect x="34" y="39" width="3" height="3"/>
<rect x="38" y="39" width="3" height="3"/>
</g>
<g fill="#222">
<rect x="2" y="6" width="3" height="3"/>
<rect x="6" y="6" width="3" height="3"/>
<rect x="10" y="6" width="3" height="3"/>
</g>
</symbol>
<!-- Sun Icon (8 rays) -->
<symbol id="icon-sun" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="4"/>
<line x1="12" y1="2" x2="12" y2="4"/>
<line x1="12" y1="20" x2="12" y2="22"/>
<line x1="4" y1="12" x2="6" y2="12"/>
<line x1="18" y1="12" x2="20" y2="12"/>
<line x1="5.64" y1="5.64" x2="7.05" y2="7.05"/>
<line x1="16.95" y1="16.95" x2="18.36" y2="18.36"/>
<line x1="5.64" y1="18.36" x2="7.05" y2="16.95"/>
<line x1="16.95" y1="7.05" x2="18.36" y2="5.64"/>
</symbol>
<!-- Moon Icon -->
<symbol id="icon-moon" viewBox="0 0 24 24">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</symbol>
<!-- Eye Icon (visible) -->
<symbol id="icon-eye-visible" viewBox="0 0 24 24">
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/>
<circle cx="12" cy="12" r="3"/>
</symbol>
<!-- Eye Icon (hidden) -->
<symbol id="icon-eye-hidden" viewBox="0 0 24 24">
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/>
<circle cx="12" cy="12" r="3"/>
<line x1="4" y1="4" x2="20" y2="20"/>
</symbol>
</defs>
</svg>
<div class="container">
<button class="theme-toggle" onclick="toggleTheme()" title="Dark Mode umschalten">
<svg id="theme-icon-sun" viewBox="0 0 24 24" style="display: none;">
<use href="#icon-sun"/>
</svg>
<svg id="theme-icon-moon" viewBox="0 0 24 24">
<use href="#icon-moon"/>
</svg>
</button>
<h1><svg class="chargraph-icon"><use href="#icon-chargraph"/></svg>CharGraph Einstellungen</h1>
<div id="specialWordsDisplay" style="text-align: center; font-size: 18px; color: var(--text-secondary); margin: 10px 0 20px 0; font-weight: 500;"></div>
<h2 onclick="toggleSection('section-info')">
<span> Projekt-Information</span>
<span class="section-toggle collapsed"></span>
</h2>
<div id="section-info" class="section-content collapsed">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px; margin-bottom: 15px;">
<h3 style="margin-top: 0; color: white; border-bottom: 2px solid rgba(255,255,255,0.3); padding-bottom: 10px;">
<svg class="chargraph-icon small" style="filter: brightness(0) invert(1);"><use href="#icon-chargraph"/></svg>
CharGraph-Projekt
</h3>
<p style="margin: 10px 0; line-height: 1.6;">
<strong>Projekt der Karl Kübel Schule in Bensheim</strong><br>
<a href="https://www.karlkuebelschule.de" target="_blank" style="color: #ffd700; text-decoration: none;">🏫 www.karlkuebelschule.de</a><br>
📅 Januar 2026
</p>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #ffc107;">
<h4 style="margin-top: 0; color: #d97706;">💰 Finanzierung</h4>
<p style="margin: 5px 0; line-height: 1.6;">
Finanziert durch Spenden an den<br>
<a href="https://www.karlkuebelschule.de/schulleben/foerderverein/" target="_blank"><strong>Förderverein der Karlkübelschule e.V.</strong></a>
</p>
</div>
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #667eea;">
<h4 style="margin-top: 0; color: #667eea;">👥 Projektteam</h4>
<p style="margin: 5px 0; line-height: 1.8;">
<strong>Projektidee und Durchführung:</strong><br>
• Simone Konrad<br>
• Philip Benz<br>
• Rainer Wieland
</p>
</div>
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #4caf50;">
<h4 style="margin-top: 0; color: #4caf50;">🌐 Web & Code</h4>
<p style="margin: 5px 0; line-height: 1.8;">
<strong>Webseite des Projektes:</strong> <a href="https://chargraph.karlkuebelschule.de" target="_blank" style="color: #667eea;">chargraph.karlkuebelschule.de</a><br>
<strong>Git Repository:</strong> <a href="https://gitea.karlkuebelschule.de/CharGraph" target="_blank" style="color: #667eea;">gitea.karlkuebelschule.de/CharGraph</a>
</p>
</div>
<div style="background: #e3f2fd; padding: 15px; border-radius: 8px; border-left: 4px solid #2196f3;">
<h4 style="margin-top: 0; color: #2196f3;">🎓 Projektbeteiligte</h4>
<p style="margin: 5px 0; line-height: 1.8; font-size: 14px;">
<strong>Schülerinnen und Schüler:</strong><br>
• Anna Müller<br>
• Ben Schmidt<br>
• Clara Weber<br>
• David Fischer<br>
• Emma Wagner<br>
• Felix Becker<br>
• Greta Hoffmann<br>
• Henry Koch<br>
• Ida Richter<br>
• Jonas Klein<br>
• Klara Schröder<br>
• Leon Neumann<br>
• Mia Braun<br>
• Noah Zimmermann<br>
• Olivia Krüger<br>
• Paul Hofmann<br>
• Quinn Schmitt<br>
• Rosa Lange<br>
• Simon Werner<br>
• Tara Schmitz
</p>
<p style="margin: 5px 0; line-height: 1.8; font-size: 14px;">
<strong>Betatester des Funkamateurclubs Weinhein FACW e.V.:</strong><br>
• DM4IM<br>
• DL9SAD<br>
• DL4ZAO<br>
• DL7UKM<br>
• DL2IAX<br>
• DJ8LC<br>
• DB2FQ<br>
</p>
</div>
</div>
<h2 onclick="toggleSection('section-time')">
<span>🕒 Zeit einstellen</span>
<span class="section-toggle collapsed"></span>
</h2>
<div id="section-time" class="section-content collapsed">
<div class="time-option">
<input type="radio" id="ntpTime" name="timeMode" value="ntp"
onchange="toggleTimeMode()" disabled>
<label for="ntpTime" title="Die Zeit wird über NTP aus dem Internet eingestellt">
Automatisch über NTP (Internet)
<span id="ntpStatusIcon" style="margin-left: 5px;">📡</span>
</label>
</div>
<div class="time-option">
<input type="radio" id="autoTime" name="timeMode" value="auto" checked
onchange="toggleTimeMode()">
<label for="autoTime">Automatisch von diesem Gerät</label>
</div>
<div class="time-option">
<input type="radio" id="manualTime" name="timeMode" value="manual"
onchange="toggleTimeMode()">
<label for="manualTime">Manuell eingeben</label>
</div>
<div class="manual-time" id="manualTimeInputs">
<div class="form-group">
<label>📅 Datum und Uhrzeit:</label>
<input type="datetime-local" id="manualDateTime">
</div>
</div>
<div class="info">
<h3><svg class="chargraph-icon small"><use href="#icon-chargraph"/></svg>CharGraph Status</h3>
<div class="time-display">
<div class="time-box">
<strong>Zeit</strong>
<div class="time-value" id="espTime">--:--:--</div>
</div>
<div class="time-box">
<strong>Ihre Geräte-Zeit</strong>
<div class="time-value" id="browserTime">--:--:--</div>
</div>
</div>
<div id="driftInfo">
<div class="drift-value">
Abweichung: <span id="driftValue">0</span> Sekunden
</div>
<div class="drift-status" id="driftStatus"></div>
</div>
<div class="details">
<div>📅 Letzter Sync: <span id="lastSync">--</span></div>
<div id="driftRateRow">📊 Drift-Rate: <span id="driftRate">--</span></div>
<div id="syncCountRow">🔢 Anzahl Syncs: <span id="syncCount">--</span></div>
<div>⏱ Laufzeit: <span id="uptime">--</span></div>
</div>
</div>
<button onclick="saveTime()" style="background: #4caf50; margin-bottom: 10px;">🕒 Aktuelle Uhrzeit speichern</button>
</div><!-- Ende section-time -->
<!-- ════════════════════════════════════════════════════════════════ -->
<!-- HELLIGKEITSANPASSUNG -->
<!-- ════════════════════════════════════════════════════════════════ -->
<h2 onclick="toggleSection('section-autobrightness')">
📊 Helligkeitsanpassung
<span class="section-toggle"></span>
</h2>
<div id="section-autobrightness" class="section-content collapsed">
<div style="background: #e3f2fd; border-left: 4px solid #2196f3; padding: 15px; margin-bottom: 20px; border-radius: 8px;">
<strong> Info:</strong> Die Helligkeit wird über einen Fotowiderstand unter dem dritten Buchstaben auf dem Panel gesteuert.
Wenn es im Raum dunkler ist, werden auch die Buchstaben weniger hell leuchten.<br>Die maximale Helligkeit ist aus Sicherheitsgründen auf 80% begrenzt.<br>
Kalibrieren Sie die Min/Max-Werte entsprechend.<br>
</div>
<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; border-radius: 8px;">
<strong>📝 Kalibrierung in 4 Schritten:</strong><br><br>
<strong>1. Dunkle Umgebung herstellen</strong><br>
• Licht ausschalten oder abdecken<br>
• Warte 2 Sekunden bis Messwert aktualisiert wird<br>
• Drücke <strong>🔽</strong> beim Minimum-Feld<br>
• LEDs leuchten mit minimaler Helligkeit<br><br>
<strong>2. Helle Umgebung herstellen</strong><br>
• Licht einschalten oder Taschenlampe verwenden<br>
• Warte 2 Sekunden bis Messwert aktualisiert wird<br>
• Drücke <strong>🔼</strong> beim Maximum-Feld<br>
• LEDs leuchten mit maximaler Helligkeit<br><br>
<strong>3. Feinabstimmung (optional)</strong><br>
• Schieberegler oder Zahleneingabe verwenden<br>
• LEDs zeigen sofort die Änderung (alle 2 Sekunden)<br>
• Eingabefelder werden während der Bearbeitung nicht überschrieben<br><br>
<strong>4. Speichern</strong><br>
• Aktiviere die Checkbox "Automatische Helligkeitsregelung"<br>
• Drücke <strong>📊 Helligkeitsanpassung speichern</strong><br>
</div>
<div class="form-group">
<label>📈 Lichtintensität:</label>
<div style="font-size: 24px; font-weight: bold; padding: 15px; background: #f5f5f5; border-radius: 8px; text-align: center;">
<span id="currentADCPercent" style="color: #2196f3;">---</span>
<span style="font-size: 18px; color: #666;">%</span>
</div>
</div>
<h3 style="margin-top: 20px; color: #666;">🔧 Umgebungslicht-Kalibrierung</h3>
<div class="form-group">
<label>Minimum (bei wenig Licht):</label>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="range" id="autoBrightnessMinADC" min="0" max="1023" value="200"
oninput="setUserAdjusting(); validateADCMin(); updateADCPreview(); sendLivePreview();"
style="flex: 1; height: 30px;">
<input type="number" id="autoBrightnessMinADCValue" min="0" max="100" value="20" readonly
style="width: 70px; padding: 8px; border: 2px solid #ddd; border-radius: 5px; text-align: center; background: #f5f5f5;">
<span style="width: 40px; text-align: center; color: #666; font-weight: bold;">%</span>
<button onclick="autoSetMin()" style="width: 40px; min-width: 40px; padding: 6px; margin: 0; background: none; border: none; cursor: pointer; font-size: 22px;" title="Aktuellen Wert als Minimum setzen">
🔽
</button>
</div>
</div>
<div class="form-group">
<label>Maximum (bei viel Licht):</label>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="range" id="autoBrightnessMaxADC" min="0" max="1023" value="820"
oninput="setUserAdjusting(); validateADCMax(); updateADCPreview(); sendLivePreview();"
style="flex: 1; height: 30px;">
<input type="number" id="autoBrightnessMaxADCValue" min="0" max="100" value="80" readonly
style="width: 70px; padding: 8px; border: 2px solid #ddd; border-radius: 5px; text-align: center; background: #f5f5f5;">
<span style="width: 40px; text-align: center; color: #666; font-weight: bold;">%</span>
<button onclick="autoSetMax()" style="width: 40px; min-width: 40px; padding: 6px; margin: 0; background: none; border: none; cursor: pointer; font-size: 22px;" title="Aktuellen Wert als Maximum setzen">
🔼
</button>
</div>
</div>
<h3 style="margin-top: 30px; color: #666;">💡 Helligkeits-Bereich</h3>
<div style="background: #e8f5e9; border-left: 4px solid #4caf50; padding: 15px; margin-bottom: 15px; border-radius: 8px; font-size: 14px;">
<strong> Info:</strong> Legen Sie fest, wie hell die LEDs bei wenig bzw. viel Licht leuchten sollen.<br>
• Bei Minimum ADC wird <strong>Minimale Helligkeit</strong> verwendet<br>
• Bei Maximum ADC wird <strong>Maximale Helligkeit</strong> verwendet<br>
• Werte dazwischen werden linear interpoliert
</div>
<div class="form-group">
<label>Minimale Anzeigehelligkeit (bei wenig Licht):</label>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="range" id="autoBrightnessMin" min="0" max="80" value="0"
oninput="document.getElementById('autoBrightnessMinValue').value = this.value; updateBrightnessDisplay(); validateBrightnessMin(); updateADCPreview();"
style="flex: 1; height: 30px;">
<input type="number" id="autoBrightnessMinValue" min="0" max="80" value="0"
oninput="document.getElementById('autoBrightnessMin').value = this.value; updateBrightnessDisplay(); validateBrightnessMin(); updateADCPreview();"
style="width: 70px; padding: 8px; border: 2px solid #ddd; border-radius: 5px; text-align: center;">
<span style="width: 40px; text-align: center; color: #666; font-weight: bold;">%</span>
<div style="width: 40px; min-width: 40px;"></div>
</div>
<small style="color: #666;">0 = Aus (0%), 80 = Maximum (80%)</small>
</div>
<div class="form-group">
<label>Maximale Anzeigehelligkeit (bei viel Licht):</label>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="range" id="autoBrightnessMax" min="0" max="80" value="80"
oninput="document.getElementById('autoBrightnessMaxValue').value = this.value; updateBrightnessDisplay(); validateBrightnessMax(); updateADCPreview();"
style="flex: 1; height: 30px;">
<input type="number" id="autoBrightnessMaxValue" min="0" max="80" value="80"
oninput="document.getElementById('autoBrightnessMax').value = this.value; updateBrightnessDisplay(); validateBrightnessMax(); updateADCPreview();"
style="width: 70px; padding: 8px; border: 2px solid #ddd; border-radius: 5px; text-align: center;">
<span style="width: 40px; text-align: center; color: #666; font-weight: bold;">%</span>
<div style="width: 40px; min-width: 40px;"></div>
</div>
<small style="color: #666;">Empfohlen: Maximal (80%)</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="autoBrightnessEnabled" style="width: auto; margin-right: 10px;">
Automatische Helligkeitsregelung aktivieren
</label>
<div id="rangeWarning" style="display: none; margin-top: 10px; padding: 10px; background: #ffebee; border-left: 4px solid #f44336; border-radius: 5px; color: #c62828; font-size: 14px;">
</div>
</div>
<button onclick="saveAutoBrightness()" style="margin-top: 10px; background: #4caf50; width: 100%;">
📊 Helligkeitsanpassung speichern
</button>
</div><!-- Ende section-autobrightness -->
<h2 onclick="toggleSection('section-wifi')">
<span>📡 WiFi & Zeit-Server Konfiguration</span>
<span class="section-toggle collapsed"></span>
</h2>
<div id="section-wifi" class="section-content collapsed">
<div style="background: #e3f2fd; border-left: 4px solid #2196f3; padding: 15px; margin-bottom: 20px; border-radius: 8px;">
<strong> Hinweis:</strong> Nach dem Speichern ist ein Neustart erforderlich, damit die WiFi-Einstellungen aktiv werden!
</div>
<div class="form-group">
<label>📊 WiFi Status:</label>
<div id="wifiStatus" style="font-size: 16px; padding: 10px; background: #f5f5f5; border-radius: 5px;">
<div>Verbindung: <span id="staConnected" style="font-weight: bold;"></span></div>
<div>IP-Adresse: <span id="staLocalIP"></span></div>
<div>Signalstärke: <span id="staRSSI"></span> dBm</div>
<div style="margin-top: 10px;">Letzter NTP-Sync: <span id="ntpLastSync"></span></div>
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="staEnabled" style="width: auto; margin-right: 10px;">
WiFi Station aktivieren (Verbindung zum WLAN)
</label>
</div>
<div class="form-group">
<label>WLAN-Name (SSID):</label>
<select id="staSsid" onchange="handleSsidSelection()" style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
<option value="">-- SSID auswählen --</option>
</select>
<input type="text" id="staSsidManual" placeholder="SSID manuell eingeben..." maxlength="31"
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box; margin-top: 10px; display: none;">
</div>
<div class="form-group">
<label>WLAN-Passwort:</label>
<div style="position: relative; display: inline-block; width: 100%;">
<input type="password" id="staPassword" placeholder="********" maxlength="63"
style="width: 100%; padding: 12px 45px 12px 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
<span onclick="togglePasswordVisibility()" id="passwordToggleIcon"
style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); cursor: pointer; user-select: none;"
title="Passwort anzeigen">
<svg class="password-toggle-icon" viewBox="0 0 24 24">
<use href="#icon-eye-hidden"/>
</svg>
</span>
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="staDhcp" checked style="width: auto; margin-right: 10px;">
DHCP verwenden (automatische IP-Adresse)
</label>
</div>
<div id="staticIPConfig" style="display: none; margin-left: 20px; padding: 15px; background: #f9f9f9; border-radius: 8px;">
<div class="form-group">
<label>IP-Adresse:</label>
<input type="text" id="staIP" placeholder="192.168.1.100" pattern="^(\d{1,3}\.){3}\d{1,3}$"
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
</div>
<div class="form-group">
<label>Gateway:</label>
<input type="text" id="staGateway" placeholder="192.168.1.1" pattern="^(\d{1,3}\.){3}\d{1,3}$"
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
</div>
<div class="form-group">
<label>Subnetzmaske:</label>
<input type="text" id="staSubnet" placeholder="255.255.255.0" pattern="^(\d{1,3}\.){3}\d{1,3}$" value="255.255.255.0"
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
</div>
<div class="form-group">
<label>DNS-Server:</label>
<input type="text" id="staDNS" placeholder="8.8.8.8" pattern="^(\d{1,3}\.){3}\d{1,3}$"
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
</div>
</div>
<div class="form-group" style="margin-top: 20px;">
<label>
<input type="checkbox" id="ntpEnabled" checked style="width: auto; margin-right: 10px;">
NTP-Zeitsynchronisation aktivieren (stündlich)
</label>
</div>
<div class="form-group">
<label>NTP-Zeitserver:</label>
<input type="text" id="ntpServer" placeholder="ptbtime1.ptb.de" maxlength="31" value="ptbtime1.ptb.de"
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
<small style="color: #666;">Standard: ptbtime1.ptb.de (Physikalisch-Technische Bundesanstalt)</small>
</div>
<button onclick="saveWiFiConfig()" style="margin-top: 10px; background: #4caf50; width: 100%;">
📡 WiFi/Zeit-Server Konfiguration speichern
</button>
</div><!-- Ende section-wifi -->
<h2 onclick="toggleSection('section-ota')">
<span>🔄 Firmware Update (OTA)</span>
<span class="section-toggle collapsed"></span>
</h2>
<div id="section-ota" class="section-content collapsed">
<div style="background: #fff9c4; border-left: 4px solid #ffc107; padding: 15px; margin-bottom: 20px; border-radius: 8px;">
<strong>⚠️ Wichtig:</strong> Das Update kann 2-5 Minuten dauern.
Unterbrechen Sie NICHT die Stromversorgung während des Updates!
</div>
<div class="form-group">
<label>📊 Aktuelle Version:</label>
<div id="currentVersion" style="font-size: 18px; font-weight: bold; color: #667eea;
padding: 10px; background: #f5f5f5; border-radius: 5px; text-align: center;">
Lädt...
</div>
</div>
<div class="form-group">
<label>Firmware-Datei hochladen:</label>
<input type="file" id="firmwareFile" accept=".bin"
style="padding: 10px; border: 2px solid #ddd; border-radius: 8px; width: 100%; box-sizing: border-box;">
<button onclick="uploadFirmware()" style="margin-top: 10px; background: #4caf50;">
📤 Firmware hochladen
</button>
</div>
<div class="form-group">
<label>Firmware-URL:</label>
<input type="text" id="firmwareURL" placeholder="http://example.com/firmware.bin"
style="width: 100%; padding: 12px; border: 2px solid #ddd;
border-radius: 8px; font-size: 16px; box-sizing: border-box;">
<button onclick="updateFromURL()" style="margin-top: 10px; background: #2196f3;">
🌐 Von URL aktualisieren
</button>
</div>
<div id="otaProgress" style="display: none; margin-top: 20px;
padding: 20px; background: #e3f2fd; border-radius: 8px;
border-left: 4px solid #2196f3;">
<div style="font-size: 18px; font-weight: bold; margin-bottom: 10px;">
Update läuft...
</div>
<div style="width: 100%; background: #ddd; border-radius: 10px;
height: 30px; overflow: hidden;">
<div id="otaProgressBar" style="width: 0%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
height: 100%; transition: width 0.3s; text-align: center;
line-height: 30px; color: white; font-weight: bold;">
0%
</div>
</div>
<div id="otaMessage" style="margin-top: 10px; font-size: 14px; color: #666;">
Starte Update...
</div>
</div>
</div><!-- Ende section-ota -->
<h2 onclick="toggleSection('section-colors')">
<span>🎨 Farbeinstellungen</span>
<span class="section-toggle collapsed"></span>
</h2>
<div id="section-colors" class="section-content collapsed">
<div class="form-group">
<label>🔤 Grundfarbe der Zeichen:</label>
<input type="color" id="normalColor" value="#FFFFFF" onchange="updatePreview()">
<div class="color-preview">
<div class="color-box" id="previewNormal"></div>
</div>
</div>
<div class="form-group">
<label>✨ Farbe für Spezialtext:</label>
<input type="color" id="specialColor" value="#FFFFFF" onchange="updatePreview()">
<div class="color-preview">
<div class="color-box" id="previewSpecial"></div>
</div>
</div>
<div class="form-group">
<label>💡 Helligkeit:</label>
<input type="range" id="brightness" min="10" max="80" value="80"
oninput="document.getElementById('brightnessValue').innerHTML = this.value">
<div class="range-value" id="brightnessValue">80</div>
</div>
<button onclick="saveColors()" style="background: #ff9800; margin-top: 10px;">🎨 Farbeinstellungen speichern</button>
</div><!-- Ende section-colors -->
<h2 onclick="toggleSection('section-charsoap')">
<span>🔤 Wortmatrix</span>
<span class="section-toggle collapsed"></span>
</h2>
<div id="section-charsoap" class="section-content collapsed">
<div class="form-group">
<label>📝 Charsoap (11×10 Zeichen):</label>
<div style="font-size: 12px; color: #666; margin-bottom: 10px;">
Großbuchstaben (A-Z), Kleinbuchstaben (a-z) und Bindestriche (-). Genau 110 Zeichen.<br>
<strong style="color: #ff9800;">💡 Tipp:</strong> Umlaute werden zu lowercase: Ä→a, Ö→o, Ü→u
</div>
<textarea id="charsoap" rows="11" maxlength="110"
oninput="validateCharsoap()"
onkeypress="return /[A-ZÄÖÜa-zäöü-]/i.test(event.key)"></textarea>
<div id="charsoap-info" style="margin-top: 10px; font-size: 13px;">
<span id="charsoap-length" style="color: #666;">Länge: 0 / 110</span>
<span id="charsoap-valid" style="margin-left: 15px;"></span>
</div>
<div style="margin-top: 10px;">
<button type="button" onclick="resetCharsoap()" class="secondary">
🔄 Standard wiederherstellen
</button>
</div>
</div>
<button onclick="saveCharsoap()" style="background: #9c27b0; margin-top: 10px;">🔤 Wortmatrix speichern</button>
</div><!-- Ende section-charsoap -->
<h2 onclick="toggleSection('section-specialwords')">
<span>✨ Spezialwörter</span>
<span class="section-toggle collapsed"></span>
</h2>
<div id="section-specialwords" class="section-content collapsed">
<div style="font-size: 13px; color: #666; margin-bottom: 15px;">
Konfiguriere bis zu 3 Spezialwörter und deren Anzeigeintervall. Max. 11 Zeichen pro Wort.
</div>
<div class="form-group">
<label>⏱️ Anzeigeintervall:</label>
<select id="specialwordinterval" style="width: 100%; padding: 8px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--input-bg); color: var(--text-primary);">
<option value="1">Jede Minute</option>
<option value="5">Alle 5 Minuten</option>
<option value="10">Alle 10 Minuten</option>
<option value="15">Alle 15 Minuten (viertel Stunde)</option>
<option value="30">Alle 30 Minuten (halbe Stunde)</option>
<option value="60" selected>Alle 60 Minuten (jede Stunde)</option>
</select>
</div>
<div class="form-group">
<label>🎯 Wort 1:</label>
<input type="text" id="specialword0" maxlength="11" placeholder="z.B. LIMONCIELLO">
</div>
<div class="form-group">
<label>🎯 Wort 2:</label>
<input type="text" id="specialword1" maxlength="11" placeholder="Optional">
</div>
<div class="form-group">
<label>🎯 Wort 3:</label>
<input type="text" id="specialword2" maxlength="11" placeholder="Optional">
</div>
<div style="margin-top: 10px; display: flex; gap: 10px;">
<button onclick="saveSpecialWords()" style="flex: 1; background: #9c27b0;">
💾 Spezialwörter speichern
</button>
<button type="button" onclick="resetSpecialWords()" class="secondary" style="flex: 1;">
🔄 Zurücksetzen
</button>
</div>
</div><!-- Ende section-specialwords -->
<h2 onclick="toggleSection('section-minuteleds')">
<span>⏰ Minuten-LEDs</span>
<span class="section-toggle collapsed"></span>
</h2>
<div id="section-minuteleds" class="section-content collapsed">
<div style="font-size: 13px; color: #666; margin-bottom: 15px;">
Konfiguriere die 4 LEDs für die Minuten-Anzeige (Werte 0-120).
</div>
<div class="form-group">
<label>💡 LED 1 (1. Minute):</label>
<input type="number" id="minuteled0" min="0" max="120" placeholder="112">
</div>
<div class="form-group">
<label>💡 LED 2 (2. Minute):</label>
<input type="number" id="minuteled1" min="0" max="120" placeholder="114">
</div>
<div class="form-group">
<label>💡 LED 3 (3. Minute):</label>
<input type="number" id="minuteled2" min="0" max="120" placeholder="116">
</div>
<div class="form-group">
<label>💡 LED 4 (4. Minute):</label>
<input type="number" id="minuteled3" min="0" max="120" placeholder="118">
</div>
<div style="margin-top: 10px; display: flex; gap: 10px;">
<button onclick="saveMinuteLeds()" style="flex: 1; background: #ff9800;">
💾 Minuten-LEDs speichern
</button>
<button type="button" onclick="resetMinuteLeds()" class="secondary" style="flex: 1;">
🔄 Zurücksetzen
</button>
</div>
</div><!-- Ende section-minuteleds -->
<h2 onclick="toggleSection('section-ledtest')">
<span>🔬 LED Test & Diagnose</span>
<span class="section-toggle collapsed"></span>
</h2>
<div id="section-ledtest" class="section-content collapsed">
<div class="form-group">
<label>Test-Modus:</label>
<div class="test-modes">
<div class="test-option">
<input type="radio" id="testSingle" name="testMode" value="single" checked>
<label for="testSingle">Einzelne LED</label>
</div>
<div class="test-option">
<input type="radio" id="testRange" name="testMode" value="range">
<label for="testRange">LED-Bereich</label>
</div>
<div class="test-option">
<input type="radio" id="testAll" name="testMode" value="all">
<label for="testAll">Alle LEDs</label>
</div>
<div class="test-option">
<input type="radio" id="testRow" name="testMode" value="row">
<label for="testRow">Zeile</label>
</div>
<div class="test-option">
<input type="radio" id="testCol" name="testMode" value="col">
<label for="testCol">Spalte</label>
</div>
</div>
</div>
<div class="form-group" id="testSingleControls">
<label>LED-Nummer (0-113):</label>
<input type="number" id="ledNumber" min="0" max="113" value="0">
</div>
<div class="form-group" id="testRangeControls" style="display: none;">
<label>Von LED:</label>
<input type="number" id="ledFrom" min="0" max="113" value="0" style="margin-bottom: 10px;">
<label>Bis LED:</label>
<input type="number" id="ledTo" min="0" max="113" value="10">
</div>
<div class="form-group" id="testRowControls" style="display: none;">
<label>Zeile (0-9):</label>
<input type="number" id="rowNumber" min="0" max="9" value="0">
</div>
<div class="form-group" id="testColControls" style="display: none;">
<label>Spalte (0-10):</label>
<input type="number" id="colNumber" min="0" max="10" value="0">
</div>
<div class="form-group">
<label>Test-Farbe:</label>
<input type="color" id="testColor" value="#FFFFFF">
</div>
<div class="form-group">
<label>Test-Helligkeit:</label>
<input type="range" id="testBrightness" min="10" max="255" value="80"
oninput="document.getElementById('testBrightnessValue').innerHTML = this.value">
<div class="range-value" id="testBrightnessValue">80</div>
</div>
<div style="display: flex; gap: 10px;">
<button onclick="testLED()" style="flex: 1; background: #4caf50;">
✅ LED(s) einschalten
</button>
<button onclick="clearLEDs()" style="flex: 1; background: #f44336;">
⭕ Alle ausschalten
</button>
</div>
<div style="margin-top: 15px; padding: 15px; background: #fff3cd; border-radius: 8px; border-left: 4px solid #ffc107;">
<strong>💡 Tipps:</strong><br>
• Matrix: LEDs 0-109<br>
• Minuten: LEDs 110-113<br>
• Zeile 0 = oben, Zeile 9 = unten<br>
• Spalte 0 = links, Spalte 10 = rechts
</div>
</div><!-- Ende section-ledtest -->
<h2 onclick="toggleSection('section-patterntest')">
<span>🎯 Pattern-Test (Kritische Zeiten)</span>
<span class="section-toggle collapsed"></span>
</h2>
<div id="section-patterntest" class="section-content collapsed">
<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin-bottom: 15px; border-radius: 8px;">
<strong> Pattern-Test:</strong><br>
Zeigt alle kritischen Uhrzeiten nacheinander für jeweils 2 Sekunden an.<br>
<strong>Getestete Fälle:</strong><br>
• Stunde 0 & 1 komplett (EIN vs EINS, mit/ohne UHR)<br>
• VIER, FÜNF, ZEHN als Stunde (Wortüberlappungen)<br>
• VIERTEL VOR VIER, FÜNF VOR FÜNF, ZEHN VOR ZEHN<br>
<strong>Dauer:</strong> ca. 2 Minuten | <strong>Anzahl:</strong> ~65 Zeiten
</div>
<div class="form-group">
<button onclick="startPatternTest()" style="padding: 12px 20px; font-size: 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; cursor: pointer;">
▶ Pattern-Test starten
</button>
</div>
<div id="patternTestStatus" style="margin-top: 15px; padding: 10px; border-radius: 5px; display: none;"></div>
</div><!-- Ende section-patterntest -->
</div><!-- Ende container -->
<script>
// ════════════════════════════════════════════════════════════════
// AUFKLAPPBARE BEREICHE
// ════════════════════════════════════════════════════════════════
function toggleSection(sectionId) {
const section = document.getElementById(sectionId);
const toggle = event.currentTarget.querySelector('.section-toggle');
const isCurrentlyCollapsed = section.classList.contains('collapsed');
// Alle anderen Sektionen schließen
if (isCurrentlyCollapsed) {
const allSections = ['section-time', 'section-wifi', 'section-ota', 'section-colors', 'section-charsoap', 'section-ledtest', 'section-patterntest', 'section-autobrightness'];
allSections.forEach(id => {
if (id !== sectionId) {
const otherSection = document.getElementById(id);
if (otherSection && !otherSection.classList.contains('collapsed')) {
otherSection.classList.add('collapsed');
// Toggle-Pfeil der anderen Sektion auch schließen
const otherHeader = otherSection.previousElementSibling;
if (otherHeader && otherHeader.tagName === 'H2') {
const otherToggle = otherHeader.querySelector('.section-toggle');
if (otherToggle) {
otherToggle.classList.add('collapsed');
}
}
}
}
});
}
// Aktuelle Sektion umschalten
section.classList.toggle('collapsed');
toggle.classList.toggle('collapsed');
// Helligkeits-Sektion Status tracken
if (sectionId === 'section-autobrightness') {
brightnessectionOpen = !section.classList.contains('collapsed');
console.log('Helligkeits-Sektion:', brightnessectionOpen ? 'geöffnet' : 'geschlossen');
} else {
// Wenn andere Sektion geöffnet wird, Helligkeits-Sektion ist geschlossen
brightnessectionOpen = false;
}
// Beim Öffnen des WiFi-Bereichs automatisch scannen
if (sectionId === 'section-wifi' && !section.classList.contains('collapsed') && !wifiScanned) {
scanWiFi(savedSsidFromConfig);
}
}
// Beim Laden alle Bereiche einklappen
window.addEventListener('load', function() {
const sections = ['section-time', 'section-wifi', 'section-ota', 'section-colors', 'section-charsoap', 'section-ledtest', 'section-patterntest', 'section-autobrightness'];
sections.forEach(id => {
const section = document.getElementById(id);
if (section) {
section.classList.add('collapsed');
// Auch den Toggle-Pfeil einklappen
const header = section.previousElementSibling;
if (header && header.tagName === 'H2') {
const toggle = header.querySelector('.section-toggle');
if (toggle) {
toggle.classList.add('collapsed');
}
}
}
});
});
// ════════════════════════════════════════════════════════════════
// ZEIT & DRIFT
// ════════════════════════════════════════════════════════════════
let espTimestamp = 0;
let browserTimestamp = 0;
function updateBrowserTime() {
const now = new Date();
document.getElementById('browserTime').innerHTML =
now.toLocaleTimeString('de-DE');
const utc = Math.floor(now.getTime() / 1000);
const offset = now.getTimezoneOffset() * 60;
browserTimestamp = utc - offset;
if (espTimestamp > 0) calculateDrift();
}
function updatePreview() {
document.getElementById('previewNormal').style.backgroundColor =
document.getElementById('normalColor').value;
document.getElementById('previewSpecial').style.backgroundColor =
document.getElementById('specialColor').value;
}
function toggleTimeMode() {
const isManual = document.getElementById('manualTime').checked;
const manualInputs = document.getElementById('manualTimeInputs');
if (isManual) {
manualInputs.style.display = 'block';
const now = new Date();
const offset = now.getTimezoneOffset() * 60000;
const localISOTime = (new Date(now - offset)).toISOString().slice(0, 16);
document.getElementById('manualDateTime').value = localISOTime;
} else {
manualInputs.style.display = 'none';
}
}
function fetchEspTime() {
fetch('/gettime')
.then(response => response.json())
.then(data => {
document.getElementById('espTime').innerHTML = data.time;
espTimestamp = data.timestamp;
calculateDrift();
updateSyncInfo(data);
updateNTPStatus(); // NTP-Status aktualisieren
})
.catch(error => {
console.error('Fehler:', error);
document.getElementById('espTime').innerHTML = 'Fehler';
});
}
function updateNTPStatus() {
// WiFi-Config abrufen, um NTP-Status zu prüfen
fetch('/wifi/config')
.then(response => response.json())
.then(data => {
const ntpRadio = document.getElementById('ntpTime');
const ntpIcon = document.getElementById('ntpStatusIcon');
const driftRateRow = document.getElementById('driftRateRow');
const syncCountRow = document.getElementById('syncCountRow');
if (data.ntpEnabled && data.staConnected) {
// NTP ist aktiviert und WiFi verbunden
ntpRadio.disabled = false;
ntpRadio.checked = true;
ntpIcon.innerHTML = data.ntpSyncOk ? '✅' : '🌐';
ntpRadio.parentElement.style.opacity = '1';
// Drift-Rate und Sync-Count ausblenden (bei NTP nicht relevant)
if (driftRateRow) driftRateRow.style.display = 'none';
if (syncCountRow) syncCountRow.style.display = 'none';
} else if (data.ntpEnabled && !data.staConnected) {
// NTP aktiviert, aber WiFi nicht verbunden
ntpRadio.disabled = true;
ntpRadio.checked = false;
ntpIcon.innerHTML = '⚠️';
ntpRadio.parentElement.style.opacity = '0.6';
document.getElementById('autoTime').checked = true;
// Drift-Rate und Sync-Count anzeigen (manuelle Synchronisation)
if (driftRateRow) driftRateRow.style.display = 'block';
if (syncCountRow) syncCountRow.style.display = 'block';
} else {
// NTP deaktiviert
ntpRadio.disabled = true;
ntpRadio.checked = false;
ntpIcon.innerHTML = '❌';
ntpRadio.parentElement.style.opacity = '0.6';
document.getElementById('autoTime').checked = true;
// Drift-Rate und Sync-Count anzeigen (manuelle Synchronisation)
if (driftRateRow) driftRateRow.style.display = 'block';
if (syncCountRow) syncCountRow.style.display = 'block';
}
})
.catch(error => {
console.error('NTP Status Fehler:', error);
});
}
function calculateDrift() {
if (espTimestamp > 0 && browserTimestamp > 0) {
const drift = espTimestamp - browserTimestamp;
const driftAbs = Math.abs(drift);
const driftInfo = document.getElementById('driftInfo');
const driftValue = document.getElementById('driftValue');
const driftStatus = document.getElementById('driftStatus');
driftInfo.style.display = 'block';
if (drift > 0) {
driftValue.innerHTML = '+' + drift;
driftStatus.innerHTML = 'Die Zeit geht ' + drift + ' Sekunden vor';
} else if (drift < 0) {
driftValue.innerHTML = drift;
driftStatus.innerHTML = 'Die Zeit geht ' + driftAbs + ' Sekunden nach';
} else {
driftValue.innerHTML = '0';
driftStatus.innerHTML = 'Perfekt synchronisiert! ✓';
}
driftInfo.className = '';
if (driftAbs <= 5) {
driftInfo.classList.add('drift-good');
} else if (driftAbs <= 30) {
driftInfo.classList.add('drift-warning');
} else {
driftInfo.classList.add('drift-bad');
}
}
}
function updateSyncInfo(data) {
if (data.lastSync > 0) {
const syncDate = new Date(data.lastSync * 1000);
const daysSince = ((Date.now() / 1000 - data.lastSync) / 86400).toFixed(1);
document.getElementById('lastSync').innerHTML =
syncDate.toLocaleString('de-DE') + ' (vor ' + daysSince + ' Tagen)';
} else {
document.getElementById('lastSync').innerHTML = 'Noch nie';
}
if (data.driftRate != 0) {
const sign = data.driftRate > 0 ? '+' : '';
document.getElementById('driftRate').innerHTML =
sign + data.driftRate + ' Sek/Tag (' + data.syncCount + ' Messungen)';
} else {
document.getElementById('driftRate').innerHTML = 'Nicht kalibriert';
}
document.getElementById('syncCount').innerHTML = data.syncCount;
const uptimeDays = (data.uptime / 86400).toFixed(1);
const uptimeHours = Math.floor((data.uptime % 86400) / 3600);
const uptimeMinutes = Math.floor((data.uptime % 3600) / 60);
document.getElementById('uptime').innerHTML =
uptimeDays + ' Tage (' + uptimeHours + 'h ' + uptimeMinutes + 'm)';
}
function validateCharsoap() {
const textarea = document.getElementById('charsoap');
const lengthSpan = document.getElementById('charsoap-length');
const validSpan = document.getElementById('charsoap-valid');
let text = textarea.value.toUpperCase();
const hadUmlauts = /[ÄÖÜ]/.test(text);
text = text.replace(/Ä/g, 'a');
text = text.replace(/Ö/g, 'o');
text = text.replace(/Ü/g, 'u');
if (textarea.value !== text) {
const cursorPos = textarea.selectionStart;
textarea.value = text;
textarea.setSelectionRange(cursorPos, cursorPos);
if (hadUmlauts) {
validSpan.innerHTML = '💡 Umlaut(e) → lowercase';
validSpan.style.color = '#ff9800';
setTimeout(() => {
validateCharsoap();
}, 2000);
return;
}
}
const length = textarea.value.length;
lengthSpan.innerHTML = 'Länge: ' + length + ' / 110';
//const valid = /^[A-Za-z\-]*$/.test(textarea.value);
//const valid = /^([A-Za-z\-]*|DL0WH)$/.test(textarea.value);
const valid = /^(?:[A-Z0-9ÄÖÜ\-]*|DL0WH)$/.test(textarea.value);
if (length === 110 && valid) {
lengthSpan.style.color = '#4caf50';
validSpan.innerHTML = '✓ Gültig';
validSpan.style.color = '#4caf50';
} else if (length > 110) {
lengthSpan.style.color = '#f44336';
validSpan.innerHTML = '❌ Zu lang!';
validSpan.style.color = '#f44336';
} else if (!valid) {
const invalidCharMatch = textarea.value.match(/[^A-Z0ÄÖÜ\-]/);
if (invalidCharMatch)
{
const position = invalidCharMatch.index;
const invalidChar = invalidCharMatch[0];
console.log(`Ungültiges Zeichen '${invalidChar}' an Position ${position}`);
}
lengthSpan.style.color = '#f44336';
validSpan.innerHTML = '❌ Ungültiges Zeichen "'+ invalidChar + '" an ' + position;
validSpan.style.color = '#f44336';
} else {
lengthSpan.style.color = '#ffc107';
validSpan.innerHTML = '⚠ Zu kurz';
validSpan.style.color = '#ffc107';
}
formatCharsoap();
}
function formatCharsoap() {
const textarea = document.getElementById('charsoap');
let text = textarea.value.replace(/\n/g, '');
if (text.length > 0) {
let formatted = '';
for (let i = 0; i < text.length; i += 11) {
if (i > 0) formatted += '\n';
formatted += text.substr(i, 11);
}
if (textarea.value !== formatted) {
const cursorPos = textarea.selectionStart;
textarea.value = formatted;
textarea.setSelectionRange(cursorPos, cursorPos);
}
}
}
function resetCharsoap() {
if (confirm('Charsoap auf Standard zurücksetzen?')) {
fetch('/resetcharsoap')
.then(response => response.text())
.then(data => {
alert('✅ Zurückgesetzt!');
loadCharsoap();
})
.catch(error => alert('❌ Fehler: ' + error));
}
}
function loadCharsoap() {
fetch('/getcharsoap')
.then(response => response.text())
.then(data => {
document.getElementById('charsoap').value = data;
validateCharsoap();
})
.catch(error => console.error('Fehler:', error));
}
// ════════════════════════════════════════════════════════════════
// SPEICHER-FUNKTIONEN (GETRENNT)
// ════════════════════════════════════════════════════════════════
function saveTime() {
let timestamp, timezoneOffset = 0;
const isManual = document.getElementById('manualTime').checked;
if (isManual) {
const manualDateTime = document.getElementById('manualDateTime').value;
if (!manualDateTime) {
alert('❌ Bitte Datum/Uhrzeit eingeben!');
return;
}
// Manuelle Eingabe wird als lokale Zeit interpretiert, zu UTC konvertieren
const localDate = new Date(manualDateTime);
const utcTimestamp = Math.floor(localDate.getTime() / 1000);
const offsetSeconds = localDate.getTimezoneOffset() * 60;
timestamp = utcTimestamp - offsetSeconds; // Zu UTC konvertieren
} else {
// Sende UTC-Zeit (Date.now() ist bereits UTC)
timestamp = Math.floor(Date.now() / 1000);
timezoneOffset = 0; // Keine Umrechnung nötig, ist bereits UTC
}
// Nur Zeit speichern (mit Dummy-Werten für Farben/Helligkeit)
const url = '/save?timestamp=' + timestamp +
'&tzoffset=' + timezoneOffset +
'&nr=255&ng=255&nb=255' + // Dummy
'&sr=255&sg=255&sb=255' + // Dummy
'&brightness=80' + // Dummy
'&charsoap='; // Leer = nicht ändern
fetch(url)
.then(response => response.text())
.then(data => {
alert('✅ Uhrzeit gespeichert!');
setTimeout(fetchEspTime, 2000);
})
.catch(error => alert('❌ Fehler beim Speichern: ' + error));
}
function saveColors() {
const normalColor = document.getElementById('normalColor').value;
const specialColor = document.getElementById('specialColor').value;
const brightness = document.getElementById('brightness').value;
const normalRGB = hexToRgb(normalColor);
const specialRGB = hexToRgb(specialColor);
// Nur Farben & Helligkeit speichern
const url = '/save?timestamp=0' + // 0 = Zeit nicht ändern
'&tzoffset=0' +
'&nr=' + normalRGB.r + '&ng=' + normalRGB.g + '&nb=' + normalRGB.b +
'&sr=' + specialRGB.r + '&sg=' + specialRGB.g + '&sb=' + specialRGB.b +
'&brightness=' + brightness +
'&charsoap='; // Leer = nicht ändern
fetch(url)
.then(response => response.text())
.then(data => {
alert('✅ Farbeinstellungen gespeichert!');
})
.catch(error => alert('❌ Fehler beim Speichern: ' + error));
}
function saveCharsoap() {
let charsoap = document.getElementById('charsoap').value;
charsoap = charsoap.replace(/\s/g, '');
if (charsoap.length !== 110) {
alert('❌ Charsoap muss 110 Zeichen lang sein!\nAktuell: ' + charsoap.length);
return;
}
if (!/^[A-Za-z0-9\-]*$/.test(charsoap)) {
alert('❌ Nur Buchstaben (A-Z, a-z) und Bindestriche (-)!');
return;
}
// Nur Charsoap speichern
const url = '/save?timestamp=0' + // 0 = Zeit nicht ändern
'&tzoffset=0' +
'&nr=255&ng=255&nb=255' + // Dummy
'&sr=255&sg=255&sb=255' + // Dummy
'&brightness=80' + // Dummy
'&charsoap=' + encodeURIComponent(charsoap);
fetch(url)
.then(response => response.text())
.then(data => {
alert('✅ Wortmatrix gespeichert!');
})
.catch(error => alert('❌ Fehler beim Speichern: ' + error));
}
function testLED() {
const mode = document.querySelector('input[name="testMode"]:checked').value;
const color = document.getElementById('testColor').value;
const brightness = document.getElementById('testBrightness').value;
const rgb = hexToRgb(color);
let url = '/ledtest?mode=' + mode;
url += '&r=' + rgb.r + '&g=' + rgb.g + '&b=' + rgb.b;
url += '&brightness=' + brightness;
if (mode === 'single') {
const ledNum = document.getElementById('ledNumber').value;
url += '&led=' + ledNum;
} else if (mode === 'range') {
const from = document.getElementById('ledFrom').value;
const to = document.getElementById('ledTo').value;
url += '&from=' + from + '&to=' + to;
} else if (mode === 'row') {
const row = document.getElementById('rowNumber').value;
url += '&row=' + row;
} else if (mode === 'col') {
const col = document.getElementById('colNumber').value;
url += '&col=' + col;
}
fetch(url)
.then(response => response.text())
.then(data => console.log('LED Test:', data))
.catch(error => alert('❌ Fehler: ' + error));
}
function clearLEDs() {
fetch('/ledtest?mode=clear')
.then(response => response.text())
.catch(error => console.error('Error:', error));
}
// ════════════════════════════════════════════════════════════════
// PATTERN TEST
// ════════════════════════════════════════════════════════════════
function startPatternTest() {
const statusDiv = document.getElementById('patternTestStatus');
statusDiv.style.display = 'block';
statusDiv.style.background = '#d1ecf1';
statusDiv.style.color = '#0c5460';
statusDiv.innerHTML = '⏳ Pattern-Test läuft... Bitte warten (ca. 2-3 Minuten)';
fetch('/patterntest')
.then(response => response.text())
.then(data => {
statusDiv.style.background = '#d4edda';
statusDiv.style.color = '#155724';
statusDiv.innerHTML = '✓ Pattern-Test abgeschlossen! Die Uhr zeigt wieder die aktuelle Zeit an.';
setTimeout(() => {
statusDiv.style.display = 'none';
}, 5000);
})
.catch(error => {
statusDiv.style.background = '#f8d7da';
statusDiv.style.color = '#721c24';
statusDiv.innerHTML = '❌ Fehler: ' + error;
});
}
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('input[name="testMode"]').forEach(radio => {
radio.addEventListener('change', function() {
document.getElementById('testSingleControls').style.display = 'none';
document.getElementById('testRangeControls').style.display = 'none';
document.getElementById('testRowControls').style.display = 'none';
document.getElementById('testColControls').style.display = 'none';
if (this.value === 'single') {
document.getElementById('testSingleControls').style.display = 'block';
} else if (this.value === 'range') {
document.getElementById('testRangeControls').style.display = 'block';
} else if (this.value === 'row') {
document.getElementById('testRowControls').style.display = 'block';
} else if (this.value === 'col') {
document.getElementById('testColControls').style.display = 'block';
}
});
});
});
// ════════════════════════════════════════════════════════════════
// OTA UPDATE FUNCTIONS
// ════════════════════════════════════════════════════════════════
function loadOTAInfo() {
fetch('/ota/info')
.then(response => response.json())
.then(data => {
const versionText = data.version +
' (' + data.buildDate + ' ' + data.buildTime + ')';
document.getElementById('currentVersion').innerHTML = versionText;
console.log('OTA Info:', data);
console.log('Free Space:', Math.round(data.freeSpace / 1024) + ' KB');
})
.catch(error => {
console.error('OTA Info Error:', error);
document.getElementById('currentVersion').innerHTML = 'Fehler beim Laden';
});
}
function uploadFirmware() {
const fileInput = document.getElementById('firmwareFile');
const file = fileInput.files[0];
if (!file) {
alert('❌ Bitte wählen Sie eine Firmware-Datei aus!');
return;
}
if (!file.name.endsWith('.bin')) {
alert('❌ Nur .bin Dateien sind erlaubt!');
return;
}
if (!confirm('Update wirklich starten?\n\nDas Gerät wird neu gestartet!')) {
return;
}
const formData = new FormData();
formData.append('firmware', file);
document.getElementById('otaProgress').style.display = 'block';
document.getElementById('otaMessage').innerHTML = 'Lade Firmware hoch...';
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percentComplete = Math.round((e.loaded / e.total) * 100);
updateOTAProgress(percentComplete, 'Hochladen: ' + percentComplete + '%');
}
});
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
updateOTAProgress(100, '✅ Update erfolgreich! Neustart in 3 Sekunden...');
setTimeout(() => {
location.reload();
}, 5000);
} else {
const response = JSON.parse(xhr.responseText);
updateOTAProgress(0, '❌ Fehler: ' + response.message);
document.getElementById('otaProgress').style.display = 'none';
}
});
xhr.addEventListener('error', function() {
updateOTAProgress(0, '❌ Netzwerkfehler beim Upload');
setTimeout(() => {
document.getElementById('otaProgress').style.display = 'none';
}, 3000);
});
xhr.open('POST', '/ota/upload');
xhr.send(formData);
pollOTAStatus();
}
function updateFromURL() {
const urlInput = document.getElementById('firmwareURL');
const url = urlInput.value.trim();
if (!url) {
alert('❌ Bitte geben Sie eine URL ein!');
return;
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
alert('❌ URL muss mit http:// oder https:// beginnen!');
return;
}
if (!confirm('Update von URL starten?\n\n' + url + '\n\nDas Gerät wird neu gestartet!')) {
return;
}
document.getElementById('otaProgress').style.display = 'block';
document.getElementById('otaMessage').innerHTML = 'Verbinde zu URL...';
fetch('/ota/url?url=' + encodeURIComponent(url))
.then(response => response.json())
.then(data => {
if (data.success) {
updateOTAProgress(0, 'Download gestartet...');
pollOTAStatus();
} else {
updateOTAProgress(0, '❌ Fehler: ' + data.message);
setTimeout(() => {
document.getElementById('otaProgress').style.display = 'none';
}, 3000);
}
})
.catch(error => {
updateOTAProgress(0, '❌ Fehler: ' + error);
setTimeout(() => {
document.getElementById('otaProgress').style.display = 'none';
}, 3000);
});
}
function updateOTAProgress(percent, message) {
const progressBar = document.getElementById('otaProgressBar');
const messageDiv = document.getElementById('otaMessage');
progressBar.style.width = percent + '%';
progressBar.innerHTML = percent + '%';
if (message) {
messageDiv.innerHTML = message;
}
if (message.includes('❌')) {
progressBar.style.background = '#f44336';
} else if (message.includes('✅')) {
progressBar.style.background = '#4caf50';
} else {
progressBar.style.background = 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)';
}
}
let otaStatusInterval = null;
function pollOTAStatus() {
if (otaStatusInterval) {
clearInterval(otaStatusInterval);
}
otaStatusInterval = setInterval(() => {
fetch('/ota/status')
.then(response => response.json())
.then(data => {
if (data.inProgress) {
const elapsedSec = Math.floor(data.elapsed / 1000);
updateOTAProgress(data.progress,
'Fortschritt: ' + data.progress + '% (' + elapsedSec + 's)');
} else {
clearInterval(otaStatusInterval);
otaStatusInterval = null;
if (data.error) {
updateOTAProgress(100, '❌ Fehler: ' + data.error);
} else if (data.progress === 100) {
updateOTAProgress(100, '✅ Update erfolgreich! Neustart...');
}
}
})
.catch(error => {
console.error('Status poll error:', error);
});
}, 500);
}
// ════════════════════════════════════════════════════════════════
// WIFI / NTP CONFIG FUNCTIONS
// ════════════════════════════════════════════════════════════════
function loadWiFiConfig() {
fetch('/wifi/config')
.then(response => response.json())
.then(data => {
// WiFi Station Settings
document.getElementById('staEnabled').checked = data.staEnabled;
// Gespeicherte SSID merken
savedSsidFromConfig = data.staSsid || '';
// Passwort setzen
document.getElementById('staPassword').value = data.staPassword || '';
document.getElementById('staDhcp').checked = data.staDhcp;
document.getElementById('staIP').value = data.staIP || '';
document.getElementById('staGateway').value = data.staGateway || '';
document.getElementById('staSubnet').value = data.staSubnet || '255.255.255.0';
document.getElementById('staDNS').value = data.staDNS || '';
// WiFi Status
document.getElementById('staConnected').innerHTML = data.staConnected ?
'<span style="color: green;">✓ Verbunden</span>' :
'<span style="color: red;">✗ Nicht verbunden</span>';
document.getElementById('staLocalIP').innerHTML = data.staLocalIP || '—';
document.getElementById('staRSSI').innerHTML = data.staConnected ? data.staRSSI : '—';
// NTP Settings
document.getElementById('ntpEnabled').checked = data.ntpEnabled;
document.getElementById('ntpServer').value = data.ntpServer || 'ptbtime1.ptb.de';
// NTP Status
if (data.ntpLastSync > 0) {
const syncDate = new Date(data.ntpLastSync * 1000);
const syncStr = syncDate.toLocaleString('de-DE');
const syncStatus = data.ntpSyncOk ? '✓' : '⚠';
document.getElementById('ntpLastSync').innerHTML = syncStatus + ' ' + syncStr;
} else {
document.getElementById('ntpLastSync').innerHTML = '—';
}
// Statische IP-Felder ein/ausblenden
toggleStaticIPFields();
console.log('WiFi Config loaded:', data);
})
.catch(error => {
console.error('WiFi Config Error:', error);
});
}
let wifiScanned = false;
let savedSsidFromConfig = '';
// Hilfsfunktion: Signalstärke als Unicode-Symbol
function getSignalSymbol(rssi) {
if (rssi > -50) return '▰▰▰▰'; // Sehr gut
if (rssi > -60) return '▰▰▰▱'; // Gut
if (rssi > -70) return '▰▰▱▱'; // Mittel
return '▰▱▱▱'; // Schwach
}
// Hilfsfunktion: Verschlüsselung als Unicode-Symbol
function getEncryptionSymbol(encType) {
if (encType === 7 || encType === 8) {
return '●'; // WPA2/WPA3 (geschlossen/sicher)
}
return '○'; // Offen/WEP (unsicher)
}
function scanWiFi(savedSsid) {
const ssidSelect = document.getElementById('staSsid');
// Symbol während des Scannens
ssidSelect.innerHTML = '<option value="">⟳ Scanne verfügbare Netzwerke...</option>';
ssidSelect.disabled = true;
fetch('/wifi/scan')
.then(response => response.json())
.then(data => {
// Dropdown leeren
ssidSelect.innerHTML = '<option value="">-- SSID auswählen --</option>';
let savedSsidFound = false;
if (data.networks && data.networks.length > 0) {
// Netzwerke nach Signalstärke sortieren (höchste zuerst)
data.networks.sort((a, b) => b.rssi - a.rssi);
// Netzwerke hinzufügen
data.networks.forEach(network => {
const option = document.createElement('option');
option.value = network.ssid;
// Unicode-Symbole für Signal und Verschlüsselung
const signalSymbol = getSignalSymbol(network.rssi);
const lockSymbol = getEncryptionSymbol(network.encryption);
// Nur Text (kein HTML) für <option>
option.text = `${signalSymbol} ${lockSymbol} ${network.ssid}`;
// Gespeicherte SSID vorselektieren
if (savedSsid && network.ssid === savedSsid) {
option.selected = true;
savedSsidFound = true;
}
ssidSelect.add(option);
});
}
// Wenn gespeicherte SSID nicht im Scan gefunden wurde, trotzdem hinzufügen
if (savedSsid && !savedSsidFound) {
const option = document.createElement('option');
option.value = savedSsid;
option.text = `${savedSsid} (gespeichert)`;
option.selected = true;
ssidSelect.insertBefore(option, ssidSelect.options[1]); // Nach "-- SSID auswählen --"
}
// "Erneut scannen" Option
const rescanOption = document.createElement('option');
rescanOption.value = '__rescan__';
rescanOption.text = '↻ Erneut scannen';
ssidSelect.add(rescanOption);
// "Manuell" Option als letzten Eintrag
const manualOption = document.createElement('option');
manualOption.value = '__manual__';
manualOption.text = '✎ Manuell eingeben...';
ssidSelect.add(manualOption);
ssidSelect.disabled = false;
wifiScanned = true;
console.log('✓ WiFi-Scan abgeschlossen: ' + (data.networks ? data.networks.length : 0) + ' Netzwerke gefunden');
})
.catch(error => {
console.error('WiFi-Scan Fehler:', error);
ssidSelect.innerHTML = '<option value="">✗ Scan fehlgeschlagen</option>';
// "Erneut scannen" und "Manuell" Option auch bei Fehler
const rescanOption = document.createElement('option');
rescanOption.value = '__rescan__';
rescanOption.text = '↻ Erneut scannen';
ssidSelect.add(rescanOption);
const manualOption = document.createElement('option');
manualOption.value = '__manual__';
manualOption.text = '✎ Manuell eingeben...';
ssidSelect.add(manualOption);
ssidSelect.disabled = false;
});
}
function handleSsidSelection() {
const ssidSelect = document.getElementById('staSsid');
const manualInput = document.getElementById('staSsidManual');
if (ssidSelect.value === '__rescan__') {
// Erneut scannen
scanWiFi(savedSsidFromConfig);
} else if (ssidSelect.value === '__manual__') {
// Manuelle Eingabe anzeigen
manualInput.style.display = 'block';
manualInput.focus();
} else {
// Manuelle Eingabe ausblenden
manualInput.style.display = 'none';
manualInput.value = '';
}
}
function togglePasswordVisibility() {
const passwordInput = document.getElementById('staPassword');
const toggleIcon = document.getElementById('passwordToggleIcon');
const svgUse = toggleIcon.querySelector('use');
if (passwordInput.type === 'password') {
// Passwort anzeigen (von Sternen zu Klartext)
passwordInput.type = 'text';
toggleIcon.title = 'Passwort verbergen';
svgUse.setAttribute('href', '#icon-eye-visible');
} else {
// Passwort verbergen (zu Sternen)
passwordInput.type = 'password';
toggleIcon.title = 'Passwort anzeigen';
svgUse.setAttribute('href', '#icon-eye-hidden');
}
}
function toggleStaticIPFields() {
const isDhcp = document.getElementById('staDhcp').checked;
document.getElementById('staticIPConfig').style.display = isDhcp ? 'none' : 'block';
}
function saveWiFiConfig() {
const params = new URLSearchParams();
// SSID: Wenn "Manuell" gewählt, dann aus manuellem Eingabefeld, sonst aus Dropdown
const ssidSelect = document.getElementById('staSsid');
const ssidManual = document.getElementById('staSsidManual');
let ssidValue = '';
if (ssidSelect.value === '__manual__') {
ssidValue = ssidManual.value;
} else if (ssidSelect.value === '__rescan__' || ssidSelect.value === '') {
// Ignoriere "Erneut scannen" und leere Auswahl
alert('⚠ Bitte wählen Sie eine SSID aus!');
return;
} else {
ssidValue = ssidSelect.value;
}
params.append('staEnabled', document.getElementById('staEnabled').checked ? '1' : '0');
params.append('staSsid', ssidValue);
params.append('staPassword', document.getElementById('staPassword').value);
params.append('staDhcp', document.getElementById('staDhcp').checked ? '1' : '0');
params.append('staIP', document.getElementById('staIP').value);
params.append('staGateway', document.getElementById('staGateway').value);
params.append('staSubnet', document.getElementById('staSubnet').value);
params.append('staDNS', document.getElementById('staDNS').value);
params.append('ntpEnabled', document.getElementById('ntpEnabled').checked ? '1' : '0');
params.append('ntpServer', document.getElementById('ntpServer').value);
fetch('/wifi/save?' + params.toString())
.then(response => response.text())
.then(data => {
// Bestätigungsdialog für Neustart
if (confirm('✓ WiFi-Konfiguration gespeichert!\n\nMöchten Sie das Gerät jetzt neu starten?\n\n(Ohne Neustart werden die WiFi-Einstellungen nicht aktiv)')) {
// Neustart durchführen
alert('Gerät wird neu gestartet...\n\nBitte warten Sie ca. 10 Sekunden.');
fetch('/restart')
.then(() => {
// Seite nach 10 Sekunden neu laden
setTimeout(() => {
location.reload();
}, 10000);
})
.catch(error => {
console.log('Neustart ausgelöst (Verbindung getrennt)');
// Seite nach 10 Sekunden neu laden
setTimeout(() => {
location.reload();
}, 10000);
});
} else {
loadWiFiConfig(); // Reload ohne Neustart
}
})
.catch(error => {
alert('❌ Fehler beim Speichern: ' + error);
});
}
// ════════════════════════════════════════════════════════════════
// AUTO-BRIGHTNESS FUNCTIONS
// ════════════════════════════════════════════════════════════════
let inputHasFocus = false;
let lastLivePreviewTime = 0;
let currentADCValue = 0;
let userIsAdjusting = false;
let adjustingTimeout = null;
let brightnessectionOpen = false;
function setUserAdjusting() {
userIsAdjusting = true;
// Timeout zurücksetzen
if (adjustingTimeout) {
clearTimeout(adjustingTimeout);
}
// Nach 5 Sekunden ohne Änderung wieder erlauben, dass Werte aktualisiert werden
adjustingTimeout = setTimeout(() => {
userIsAdjusting = false;
console.log('Auto-Update wieder aktiviert');
}, 5000);
}
function loadAutoBrightnessConfig() {
fetch('/autobrightness/config')
.then(response => response.json())
.then(data => {
// Config laden
document.getElementById('autoBrightnessEnabled').checked = data.enabled;
// Nur aktualisieren, wenn:
// - Benutzer nicht gerade Werte anpasst
// - Helligkeits-Sektion nicht geöffnet ist
if (!inputHasFocus && !userIsAdjusting && !brightnessectionOpen) {
document.getElementById('autoBrightnessMinADC').value = data.minADC || 200;
document.getElementById('autoBrightnessMinADCValue').value = data.minADC || 200;
document.getElementById('autoBrightnessMaxADC').value = data.maxADC || 820;
document.getElementById('autoBrightnessMaxADCValue').value = data.maxADC || 820;
// Helligkeits-Bereich laden
document.getElementById('autoBrightnessMin').value = data.minBrightness || 0;
document.getElementById('autoBrightnessMinValue').value = data.minBrightness || 0;
document.getElementById('autoBrightnessMax').value = data.maxBrightness || 80;
document.getElementById('autoBrightnessMaxValue').value = data.maxBrightness || 80;
// Spannungen und Prozente aktualisieren
updateADCPreview();
updateBrightnessDisplay();
}
// Aktuellen ADC-Wert als Prozent anzeigen
currentADCValue = data.currentADC || 0;
const adcPercent = Math.round(currentADCValue / 1023 * 100);
document.getElementById('currentADCPercent').innerHTML = currentADCValue > 0 ? adcPercent : '---';
console.log('Auto-Brightness Config loaded:', data);
})
.catch(error => {
console.error('Auto-Brightness Config Error:', error);
});
}
function saveAutoBrightness() {
const minADC = parseInt(document.getElementById('autoBrightnessMinADC').value);
const maxADC = parseInt(document.getElementById('autoBrightnessMaxADC').value);
const enabled = document.getElementById('autoBrightnessEnabled').checked;
// Validierung: Mindestdifferenz erforderlich
const adcRange = maxADC - minADC;
const MIN_RANGE = 20;
if (enabled && adcRange < MIN_RANGE) {
alert('⚠️ Automatische Helligkeitsregelung kann nicht aktiviert werden!\n\n' +
'Der Bereich (Max - Min) ist zu klein: ' + adcRange + '\n' +
'Mindestens erforderlich: ' + MIN_RANGE + '\n\n' +
'Bitte passen Sie die Min/Max-Werte an oder deaktivieren Sie die automatische Regelung.');
// Checkbox automatisch deaktivieren
document.getElementById('autoBrightnessEnabled').checked = false;
return;
}
const params = new URLSearchParams();
params.append('enabled', document.getElementById('autoBrightnessEnabled').checked ? '1' : '0');
params.append('minADC', minADC);
params.append('maxADC', maxADC);
params.append('minBrightness', parseInt(document.getElementById('autoBrightnessMin').value));
params.append('maxBrightness', parseInt(document.getElementById('autoBrightnessMax').value));
fetch('/autobrightness/save?' + params.toString())
.then(response => response.text())
.then(data => {
alert('✓ ' + data);
// Nach Speichern: Auto-Update wieder erlauben
userIsAdjusting = false;
if (adjustingTimeout) {
clearTimeout(adjustingTimeout);
adjustingTimeout = null;
}
loadAutoBrightnessConfig(); // Reload
})
.catch(error => {
alert('❌ Fehler beim Speichern: ' + error);
});
}
// ════════════════════════════════════════════════════════════════
// BRIGHTNESS VALIDIERUNG & ANZEIGE
// ════════════════════════════════════════════════════════════════
function updateBrightnessDisplay() {
// Diese Funktion wird nicht mehr benötigt, da die %-Anzeigen statisch sind
// Wird aus Kompatibilität beibehalten, aber macht nichts mehr
}
function validateBrightnessMin() {
const minInput = document.getElementById('autoBrightnessMin');
const maxInput = document.getElementById('autoBrightnessMax');
const minVal = parseInt(minInput.value);
const maxVal = parseInt(maxInput.value);
// Wenn Min >= Max: Max automatisch erhöhen
if (minVal >= maxVal) {
const newMax = Math.min(80, minVal + 5); // Max 80
maxInput.value = newMax;
document.getElementById('autoBrightnessMaxValue').value = newMax;
updateBrightnessDisplay();
}
}
function validateBrightnessMax() {
const minInput = document.getElementById('autoBrightnessMin');
const maxInput = document.getElementById('autoBrightnessMax');
const minVal = parseInt(minInput.value);
const maxVal = parseInt(maxInput.value);
// Max darf nicht > 80 sein
if (maxVal > 80) {
maxInput.value = 80;
document.getElementById('autoBrightnessMaxValue').value = 80;
}
// Wenn Max <= Min: Min automatisch verringern
if (maxVal <= minVal && maxVal > 0) {
const newMin = Math.max(0, maxVal - 5);
minInput.value = newMin;
document.getElementById('autoBrightnessMinValue').value = newMin;
}
updateBrightnessDisplay();
}
function validateADCMin() {
const minInput = document.getElementById('autoBrightnessMinADC');
const maxInput = document.getElementById('autoBrightnessMaxADC');
const minVal = parseInt(minInput.value);
const maxVal = parseInt(maxInput.value);
// Wenn Min >= Max: Max automatisch erhöhen
if (minVal >= maxVal) {
const newMax = Math.min(1023, minVal + 20);
maxInput.value = newMax;
document.getElementById('autoBrightnessMaxADCValue').value = newMax;
}
}
function validateADCMax() {
const minInput = document.getElementById('autoBrightnessMinADC');
const maxInput = document.getElementById('autoBrightnessMaxADC');
const minVal = parseInt(minInput.value);
const maxVal = parseInt(maxInput.value);
// Wenn Max <= Min: Min automatisch verringern
if (maxVal <= minVal && maxVal > 0) {
const newMin = Math.max(0, maxVal - 20);
minInput.value = newMin;
document.getElementById('autoBrightnessMinADCValue').value = newMin;
}
}
function updateADCPreview() {
// Slider und Number-Input synchronisieren
const minSlider = document.getElementById('autoBrightnessMinADC');
const minValue = document.getElementById('autoBrightnessMinADCValue');
const maxSlider = document.getElementById('autoBrightnessMaxADC');
const maxValue = document.getElementById('autoBrightnessMaxADCValue');
// Prozent berechnen (0-1023 = 0-100%)
const minPercent = Math.round(minSlider.value / 1023 * 100);
const maxPercent = Math.round(maxSlider.value / 1023 * 100);
minValue.value = minPercent;
maxValue.value = maxPercent;
// Bereich-Validierung anzeigen (nur Warnung, keine Auto-Deaktivierung)
const adcRange = parseInt(maxSlider.value) - parseInt(minSlider.value);
const MIN_RANGE = 20;
const rangeWarning = document.getElementById('rangeWarning');
if (adcRange < MIN_RANGE) {
// Nur Warnung anzeigen, Checkbox NICHT deaktivieren
if (rangeWarning) {
rangeWarning.style.display = 'block';
rangeWarning.innerHTML = '⚠️ Bereich zu klein (' + adcRange + ' < ' + MIN_RANGE + '). Automatische Regelung kann nicht aktiviert werden.';
}
} else {
// Warnung ausblenden
if (rangeWarning) {
rangeWarning.style.display = 'none';
}
}
}
function autoSetMin() {
if (currentADCValue > 0) {
// Verhindere automatisches Überschreiben
setUserAdjusting();
document.getElementById('autoBrightnessMinADC').value = currentADCValue;
document.getElementById('autoBrightnessMinADCValue').value = currentADCValue;
updateADCPreview();
sendLivePreview();
// Visuelles Feedback
const btn = event.target;
const originalText = btn.innerHTML;
btn.innerHTML = '✓ Gesetzt';
btn.style.background = '#4caf50';
setTimeout(() => {
btn.innerHTML = originalText;
btn.style.background = '#999';
}, 1000);
console.log('Min-Wert gesetzt auf:', currentADCValue);
} else {
alert('⚠ Kein aktueller Wert verfügbar. Bitte warten Sie auf die nächste Messung.');
}
}
function autoSetMax() {
if (currentADCValue > 0) {
// Verhindere automatisches Überschreiben
setUserAdjusting();
document.getElementById('autoBrightnessMaxADC').value = currentADCValue;
document.getElementById('autoBrightnessMaxADCValue').value = currentADCValue;
updateADCPreview();
sendLivePreview();
// Visuelles Feedback
const btn = event.target;
const originalText = btn.innerHTML;
btn.innerHTML = '✓ Gesetzt';
btn.style.background = '#4caf50';
setTimeout(() => {
btn.innerHTML = originalText;
btn.style.background = '#999';
}, 1000);
console.log('Max-Wert gesetzt auf:', currentADCValue);
} else {
alert('⚠ Kein aktueller Wert verfügbar. Bitte warten Sie auf die nächste Messung.');
}
}
function sendLivePreview() {
// Throttle: Nur alle 500ms senden für schnelleres Feedback
const now = Date.now();
if (now - lastLivePreviewTime < 500) {
return;
}
lastLivePreviewTime = now;
// Berechne Helligkeit basierend auf eingestellten Werten
const minADC = parseInt(document.getElementById('autoBrightnessMinADC').value);
const maxADC = parseInt(document.getElementById('autoBrightnessMaxADC').value);
// Validierung
if (maxADC <= minADC) {
console.error('MaxADC muss größer als MinADC sein!');
return;
}
// Simuliere eine mittlere Helligkeit basierend auf den eingestellten Werten
// Verwende die Mitte des Bereichs für die Vorschau
let simulatedADC = Math.round((minADC + maxADC) / 2);
// Wenn currentADCValue verfügbar ist, verwende ihn stattdessen
let adc = (currentADCValue > 0) ? currentADCValue : simulatedADC;
if (adc < minADC) adc = minADC;
if (adc > maxADC) adc = maxADC;
// Helligkeit berechnen: geringer ADC = geringe Helligkeit
// Wenn minADC sehr klein ist (< 10), können LEDs komplett aus sein (für Schlafzimmer)
// Sonst Minimum 5% damit man noch was sieht
const minBrightness = (minADC < 10) ? 0 : 10;
const maxBrightness = 204; // 80% Maximum
let brightness = minBrightness + Math.round((adc - minADC) * (maxBrightness - minBrightness) / (maxADC - minADC));
// Sicherheitsprüfung
if (brightness < 0) brightness = 0;
if (brightness > maxBrightness) brightness = maxBrightness;
// Aktuelle Farben aus den Farbeinstellungen holen (falls vorhanden)
let r = 255, g = 255, b = 255;
const normalColorInput = document.getElementById('normalColor');
if (normalColorInput) {
const rgb = hexToRgb(normalColorInput.value);
if (rgb) {
r = rgb.r;
g = rgb.g;
b = rgb.b;
}
}
// Sende Vorschau-Befehl an ESP (alle LEDs)
fetch('/ledtest?mode=all&r=' + r + '&g=' + g + '&b=' + b + '&brightness=' + brightness)
.then(response => {
console.log('Live-Vorschau: ADC=' + adc + ', Helligkeit=' + brightness + ' (' + Math.round(brightness/204*100) + '%), RGB=(' + r + ',' + g + ',' + b + ')');
})
.catch(error => {
console.error('Live-Vorschau Fehler:', error);
});
}
window.addEventListener('load', function() {
updatePreview();
updateBrowserTime();
fetchEspTime();
loadCharsoap();
loadSpecialWords();
loadMinuteLeds();
loadOTAInfo();
loadWiFiConfig();
loadAutoBrightnessConfig();
setInterval(updateBrowserTime, 1000);
setInterval(fetchEspTime, 5000);
setInterval(loadOTAInfo, 60000);
setInterval(loadWiFiConfig, 30000);
setInterval(loadAutoBrightnessConfig, 2000); // Auto-Brightness alle 2s aktualisieren
// Event Listener für DHCP-Checkbox
document.getElementById('staDhcp').addEventListener('change', toggleStaticIPFields);
const now = new Date();
const offset = now.getTimezoneOffset() * 60000;
const localISOTime = (new Date(now - offset)).toISOString().slice(0, 16);
document.getElementById('manualDateTime').value = localISOTime;
// Dark Mode: Gespeicherten Modus laden
initTheme();
});
// ════════════════════════════════════════════════════════════════
// DARK MODE TOGGLE
// ════════════════════════════════════════════════════════════════
function initTheme() {
// Gespeicherten Modus aus localStorage laden
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Dark Mode aktivieren wenn:
// 1. Explizit gespeichert als 'dark' ODER
// 2. Nichts gespeichert UND System bevorzugt Dark Mode
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
document.body.classList.add('dark-mode');
updateThemeIcon(true);
} else {
updateThemeIcon(false);
}
}
function toggleTheme() {
const isDark = document.body.classList.toggle('dark-mode');
// Im localStorage speichern
localStorage.setItem('theme', isDark ? 'dark' : 'light');
// Icon aktualisieren
updateThemeIcon(isDark);
}
function updateThemeIcon(isDark) {
const sunIcon = document.getElementById('theme-icon-sun');
const moonIcon = document.getElementById('theme-icon-moon');
if (isDark) {
// Dark Mode aktiv → Sonne anzeigen (zum Umschalten zu Hell)
sunIcon.style.display = 'block';
moonIcon.style.display = 'none';
} else {
// Light Mode aktiv → Mond anzeigen (zum Umschalten zu Dark)
sunIcon.style.display = 'none';
moonIcon.style.display = 'block';
}
}
// ════════════════════════════════════════════════════════════════
// SPECIAL WORD FUNKTIONEN
// ════════════════════════════════════════════════════════════════
function loadSpecialWords() {
fetch('/specialwords/get')
.then(response => response.json())
.then(data => {
for (let i = 0; i < 3; i++) {
document.getElementById('specialword' + i).value = data.words[i] || '';
}
// Intervall setzen
if (data.interval) {
document.getElementById('specialwordinterval').value = data.interval;
}
// Spezialwörter unter der Überschrift anzeigen
const displayElement = document.getElementById('specialWordsDisplay');
const nonEmptyWords = data.words.filter(word => word && word.trim() !== '');
if (nonEmptyWords.length > 0) {
displayElement.textContent = nonEmptyWords.join(' ');
} else {
displayElement.textContent = '';
}
})
.catch(error => console.error('Fehler beim Laden der Spezialwörter:', error));
}
function saveSpecialWords() {
const words = [];
for (let i = 0; i < 3; i++) {
const word = document.getElementById('specialword' + i).value.trim();
if (word.length > 11) {
alert('❌ Wort ' + (i+1) + ' zu lang (max 11 Zeichen)!');
return;
}
words.push(word);
}
const params = new URLSearchParams();
for (let i = 0; i < 3; i++) {
params.append('word' + i, words[i]);
}
// Intervall hinzufügen
const interval = document.getElementById('specialwordinterval').value;
params.append('interval', interval);
fetch('/specialwords/save?' + params.toString())
.then(response => response.text())
.then(data => {
alert('✅ Spezialwörter gespeichert!');
loadSpecialWords(); // Anzeige aktualisieren
})
.catch(error => alert('❌ Fehler beim Speichern: ' + error));
}
function resetSpecialWords() {
if (confirm('Spezialwörter auf Standard zurücksetzen?')) {
fetch('/specialwords/reset')
.then(response => response.text())
.then(data => {
alert('✅ Zurückgesetzt!');
loadSpecialWords();
})
.catch(error => alert('❌ Fehler: ' + error));
}
}
// ════════════════════════════════════════════════════════════════
// MINUTE LEDS FUNKTIONEN
// ════════════════════════════════════════════════════════════════
function loadMinuteLeds() {
fetch('/minuteleds/get')
.then(response => response.json())
.then(data => {
for (let i = 0; i < 4; i++) {
document.getElementById('minuteled' + i).value = data.leds[i];
}
})
.catch(error => console.error('Fehler beim Laden der Minuten-LEDs:', error));
}
function saveMinuteLeds() {
const leds = [];
for (let i = 0; i < 4; i++) {
const value = parseInt(document.getElementById('minuteled' + i).value);
if (isNaN(value) || value < 0 || value > 120) {
alert('❌ LED ' + (i+1) + ' ungültig (0-120)!');
return;
}
leds.push(value);
}
const params = new URLSearchParams();
for (let i = 0; i < 4; i++) {
params.append('led' + i, leds[i]);
}
fetch('/minuteleds/save?' + params.toString())
.then(response => response.text())
.then(data => {
alert('✅ Minuten-LEDs gespeichert!');
})
.catch(error => alert('❌ Fehler beim Speichern: ' + error));
}
function resetMinuteLeds() {
if (confirm('Minuten-LEDs auf Standard zurücksetzen?')) {
fetch('/minuteleds/reset')
.then(response => response.text())
.then(data => {
alert('✅ Zurückgesetzt!');
loadMinuteLeds();
})
.catch(error => alert('❌ Fehler: ' + error));
}
}
</script>
</body>
</html>