From b97880bcf120a5c0451d185526f13df419b0cb66 Mon Sep 17 00:00:00 2001 From: M-Factory Date: Fri, 17 Oct 2025 17:31:07 +0900 Subject: [PATCH] Captive portal update, Pinout update and API obfuscation Captive portal will now open automatically in most devices. Some user were concerned that their API key was visible so it is now obfuscated. Pinout has been updated to better fit all the Wemos boards. --- ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino | 33 +- ESPTimeCast_ESP32/data/index.html | 541 +++++++++++--------- ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino | 31 +- ESPTimeCast_ESP8266/data/index.html | 541 +++++++++++--------- assets/wiring3.png | Bin 0 -> 75893 bytes 5 files changed, 652 insertions(+), 494 deletions(-) create mode 100644 assets/wiring3.png diff --git a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino index 82829f8..c9882d6 100644 --- a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino +++ b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino @@ -20,9 +20,9 @@ #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 -#define CLK_PIN 9 -#define CS_PIN 11 -#define DATA_PIN 12 +#define CLK_PIN 7 //D5 +#define CS_PIN 11 // D7 +#define DATA_PIN 12 //D8 MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES); AsyncWebServer server(80); @@ -137,7 +137,7 @@ const unsigned long descriptionDuration = 3000; // 3s for short text static unsigned long descScrollEndTime = 0; // for post-scroll delay (re-used for scroll timing) const unsigned long descriptionScrollPause = 300; // 300ms pause after scroll -// --- Safe WiFi credential getters --- +// --- Safe WiFi credential and API getters --- const char *getSafeSsid() { return isAPMode ? "" : ssid; } @@ -150,6 +150,14 @@ const char *getSafePassword() { } } +const char *getSafeApiKey() { + if (strlen(openWeatherApiKey) == 0) { + return ""; + } else { + return "********************************"; // Always masked, even in AP mode + } +} + // Scroll flipped textEffect_t getEffectiveScrollDirection(textEffect_t desiredDirection, bool isFlipped) { if (isFlipped) { @@ -598,6 +606,7 @@ void setupWebServer() { // Always sanitize before sending to browser doc[F("ssid")] = getSafeSsid(); doc[F("password")] = getSafePassword(); + doc[F("openWeatherApiKey")] = getSafeApiKey(); doc[F("mode")] = isAPMode ? "ap" : "sta"; String response; @@ -644,15 +653,24 @@ void setupWebServer() { else if (n == "showWeatherDescription") doc[n] = (v == "true" || v == "on" || v == "1"); else if (n == "dimmingEnabled") doc[n] = (v == "true" || v == "on" || v == "1"); else if (n == "weatherUnits") doc[n] = v; - else if (n == "password") { + else if (n == "password") { if (v != "********" && v.length() > 0) { doc[n] = v; // user entered a new password } else { Serial.println(F("[SAVE] Password unchanged.")); // do nothing, keep the one already in doc } + } + else if (n == "openWeatherApiKey") { + if (v != "********************************") { // ignore mask only + doc[n] = v; // save new key (even if empty) + Serial.print(F("[SAVE] API key updated: ")); + Serial.println(v.length() == 0 ? "(empty)" : v); + } else { + Serial.println(F("[SAVE] API key unchanged (mask ignored).")); + } } else { doc[n] = v; } @@ -1056,7 +1074,10 @@ void setupWebServer() { request->send(200, "application/json", "{\"ok\":true}"); }); - + server.on("/generate_204", HTTP_GET, handleCaptivePortal); // Android + server.on("/fwlink", HTTP_GET, handleCaptivePortal); // Windows + server.on("/hotspot-detect.html", HTTP_GET, handleCaptivePortal); // iOS/macOS + server.onNotFound(handleCaptivePortal); server.begin(); Serial.println(F("[WEBSERVER] Web server started")); } diff --git a/ESPTimeCast_ESP32/data/index.html b/ESPTimeCast_ESP32/data/index.html index d25eca5..d6b14d9 100644 --- a/ESPTimeCast_ESP32/data/index.html +++ b/ESPTimeCast_ESP32/data/index.html @@ -679,160 +679,149 @@ window.onbeforeunload = function () { window.onload = function () { fetch('/config.json') - .then(response => response.json()) - .then(data => { - isAPMode = (data.mode === "ap"); - if (isAPMode) { - document.querySelector('.geo-note').style.display = 'block'; - document.getElementById('geo-button').classList.add('geo-disabled'); - document.getElementById('geo-button').disabled = true; - } + .then(response => response.json()) + .then(data => { + isAPMode = (data.mode === "ap"); + if (isAPMode) { + document.querySelector('.geo-note').style.display = 'block'; + document.getElementById('geo-button').classList.add('geo-disabled'); + document.getElementById('geo-button').disabled = true; + } + document.getElementById('ssid').value = data.ssid || ''; + document.getElementById('password').value = data.password || ''; + const apiInput = document.getElementById('openWeatherApiKey'); + if (data.openWeatherApiKey && data.openWeatherApiKey.trim() !== '') { + apiInput.value = MASK; + hasSavedKey = true; // mark it as having a saved key + } else { + apiInput.value = ''; + hasSavedKey = false; + } + document.getElementById('openWeatherCity').value = data.openWeatherCity || ''; + document.getElementById('openWeatherCountry').value = data.openWeatherCountry || ''; + document.getElementById('weatherUnits').checked = (data.weatherUnits === "imperial"); + document.getElementById('clockDuration').value = (data.clockDuration || 10000) / 1000; + document.getElementById('weatherDuration').value = (data.weatherDuration || 5000) / 1000; + document.getElementById('language').value = data.language || ''; - document.getElementById('ssid').value = data.ssid || ''; - document.getElementById('password').value = data.password || ''; - document.getElementById('openWeatherApiKey').value = data.openWeatherApiKey || ''; - document.getElementById('openWeatherCity').value = data.openWeatherCity || ''; - document.getElementById('openWeatherCountry').value = data.openWeatherCountry || ''; - document.getElementById('weatherUnits').checked = (data.weatherUnits === "imperial"); - document.getElementById('clockDuration').value = (data.clockDuration || 10000) / 1000; - document.getElementById('weatherDuration').value = (data.weatherDuration || 5000) / 1000; - document.getElementById('language').value = data.language || ''; - // Advanced: - document.getElementById('brightnessSlider').value = typeof data.brightness !== "undefined" ? data.brightness : 10; - document.getElementById('brightnessValue').textContent = (document.getElementById('brightnessSlider').value == -1 ? 'Off' : document.getElementById('brightnessSlider').value); - document.getElementById('flipDisplay').checked = !!data.flipDisplay; - document.getElementById('ntpServer1').value = data.ntpServer1 || ""; - document.getElementById('ntpServer2').value = data.ntpServer2 || ""; - document.getElementById('twelveHourToggle').checked = !!data.twelveHourToggle; - document.getElementById('showDayOfWeek').checked = !!data.showDayOfWeek; - document.getElementById('showDate').checked = !!data.showDate; - document.getElementById('showHumidity').checked = !!data.showHumidity; - document.getElementById('colonBlinkEnabled').checked = !!data.colonBlinkEnabled; - document.getElementById('showWeatherDescription').checked = !!data.showWeatherDescription; - // Dimming controls -const dimmingEnabledEl = document.getElementById('dimmingEnabled'); -const isDimming = (data.dimmingEnabled === true || data.dimmingEnabled === "true" || data.dimmingEnabled === 1); -dimmingEnabledEl.checked = isDimming; + // Advanced: + document.getElementById('brightnessSlider').value = typeof data.brightness !== "undefined" ? data.brightness : 10; + document.getElementById('brightnessValue').textContent = (document.getElementById('brightnessSlider').value == -1 ? 'Off' : document.getElementById('brightnessSlider').value); + document.getElementById('flipDisplay').checked = !!data.flipDisplay; + document.getElementById('ntpServer1').value = data.ntpServer1 || ""; + document.getElementById('ntpServer2').value = data.ntpServer2 || ""; + document.getElementById('twelveHourToggle').checked = !!data.twelveHourToggle; + document.getElementById('showDayOfWeek').checked = !!data.showDayOfWeek; + document.getElementById('showDate').checked = !!data.showDate; + document.getElementById('showHumidity').checked = !!data.showHumidity; + document.getElementById('colonBlinkEnabled').checked = !!data.colonBlinkEnabled; + document.getElementById('showWeatherDescription').checked = !!data.showWeatherDescription; -// Defer field enabling until checkbox state is rendered -setTimeout(() => { - setDimmingFieldsEnabled(dimmingEnabledEl.checked); -}, 0); - -dimmingEnabledEl.addEventListener('change', function () { - setDimmingFieldsEnabled(this.checked); -}); - - document.getElementById('dimStartTime').value = - (data.dimStartHour !== undefined ? String(data.dimStartHour).padStart(2, '0') : "18") + ":" + - (data.dimStartMinute !== undefined ? String(data.dimStartMinute).padStart(2, '0') : "00"); - -document.getElementById('dimEndTime').value = - (data.dimEndHour !== undefined ? String(data.dimEndHour).padStart(2, '0') : "08") + ":" + - (data.dimEndMinute !== undefined ? String(data.dimEndMinute).padStart(2, '0') : "00"); - - document.getElementById('dimBrightness').value = (data.dimBrightness !== undefined ? data.dimBrightness : 2); - // Then update the span's text content with that value - document.getElementById('dimmingBrightnessValue').textContent = (document.getElementById('dimBrightness').value == -1 ? 'Off' : document.getElementById('dimBrightness').value); - - setDimmingFieldsEnabled(!!data.dimmingEnabled); - - // --- NEW: Populate Countdown Fields --- - document.getElementById('isDramaticCountdown').checked = !!(data.countdown && data.countdown.isDramaticCountdown); - const countdownEnabledEl = document.getElementById('countdownEnabled'); // Get reference - countdownEnabledEl.checked = !!(data.countdown && data.countdown.enabled); - - if (data.countdown && data.countdown.targetTimestamp) { - // Convert Unix timestamp (seconds) to milliseconds for JavaScript Date object - const targetDate = new Date(data.countdown.targetTimestamp * 1000); - const year = targetDate.getFullYear(); - // Month is 0-indexed in JS, so add 1 - const month = (targetDate.getMonth() + 1).toString().padStart(2, '0'); - const day = targetDate.getDate().toString().padStart(2, '0'); - const hours = targetDate.getHours().toString().padStart(2, '0'); - const minutes = targetDate.getMinutes().toString().padStart(2, '0'); - - document.getElementById('countdownDate').value = `${year}-${month}-${day}`; - document.getElementById('countdownTime').value = `${hours}:${minutes}`; - } else { - // Clear fields if no target timestamp is set - document.getElementById('countdownDate').value = ''; - document.getElementById('countdownTime').value = ''; - } - // --- END NEW --- - - // --- NEW: Countdown Label Input Validation --- - const countdownLabelInput = document.getElementById('countdownLabel'); - countdownLabelInput.addEventListener('input', function() { - // Convert to uppercase and remove any characters that are not A-Z or space - // Note: The `maxlength` attribute in HTML handles the length limit. - this.value = this.value.toUpperCase().replace(/[^A-Z0-9 :!'.\-]/g, ''); - }); - // Set initial value for countdownLabel from config.json (apply validation on load too) - if (data.countdown && data.countdown.label) { - countdownLabelInput.value = data.countdown.label.toUpperCase().replace(/[^A-Z0-9 :!'.\-]/g, ''); - } else { - countdownLabelInput.value = ''; - } - // --- END NEW --- - - - // --- NEW: Countdown Toggle Event Listener & Field Enabling --- - // If you're using onchange="setCountdownEnabled(this.checked)" directly in HTML, - // you would add setCountdownFieldsEnabled(this.checked) there as well. - // If you are using addEventListener, keep this: - countdownEnabledEl.addEventListener('change', function() { - setCountdownEnabled(this.checked); // Sends command to ESP - setCountdownFieldsEnabled(this.checked); // Enables/disables local fields - }); - -const dramaticCountdownEl = document.getElementById('isDramaticCountdown'); -dramaticCountdownEl.addEventListener('change', function () { - setIsDramaticCountdown(this.checked); -}); - - // Set initial state of fields when page loads - setCountdownFieldsEnabled(countdownEnabledEl.checked); - // --- END NEW --- - - // Auto-detect browser's timezone if not set in config - if (!data.timeZone) { - try { - const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; - if ( - tz && - document.getElementById('timeZone').querySelector(`[value="${tz}"]`) - ) { - document.getElementById('timeZone').value = tz; - } else { - document.getElementById('timeZone').value = ''; - } - } catch (e) { + // Dimming controls + const dimmingEnabledEl = document.getElementById('dimmingEnabled'); + const isDimming = (data.dimmingEnabled === true || data.dimmingEnabled === "true" || data.dimmingEnabled === 1); + dimmingEnabledEl.checked = isDimming; + // Defer field enabling until checkbox state is rendered + setTimeout(() => { + setDimmingFieldsEnabled(dimmingEnabledEl.checked); + }, 0); + dimmingEnabledEl.addEventListener('change', function () { + setDimmingFieldsEnabled(this.checked); + }); + document.getElementById('dimStartTime').value = + (data.dimStartHour !== undefined ? String(data.dimStartHour).padStart(2, '0') : "18") + ":" + + (data.dimStartMinute !== undefined ? String(data.dimStartMinute).padStart(2, '0') : "00"); + document.getElementById('dimEndTime').value = + (data.dimEndHour !== undefined ? String(data.dimEndHour).padStart(2, '0') : "08") + ":" + + (data.dimEndMinute !== undefined ? String(data.dimEndMinute).padStart(2, '0') : "00"); + document.getElementById('dimBrightness').value = (data.dimBrightness !== undefined ? data.dimBrightness : 2); + // Then update the span's text content with that value + document.getElementById('dimmingBrightnessValue').textContent = (document.getElementById('dimBrightness').value == -1 ? 'Off' : document.getElementById('dimBrightness').value); + setDimmingFieldsEnabled(!!data.dimmingEnabled); + // --- Populate Countdown Fields --- + document.getElementById('isDramaticCountdown').checked = !!(data.countdown && data.countdown.isDramaticCountdown); + const countdownEnabledEl = document.getElementById('countdownEnabled'); // Get reference + countdownEnabledEl.checked = !!(data.countdown && data.countdown.enabled); + if (data.countdown && data.countdown.targetTimestamp) { + // Convert Unix timestamp (seconds) to milliseconds for JavaScript Date object + const targetDate = new Date(data.countdown.targetTimestamp * 1000); + const year = targetDate.getFullYear(); + // Month is 0-indexed in JS, so add 1 + const month = (targetDate.getMonth() + 1).toString().padStart(2, '0'); + const day = targetDate.getDate().toString().padStart(2, '0'); + const hours = targetDate.getHours().toString().padStart(2, '0'); + const minutes = targetDate.getMinutes().toString().padStart(2, '0'); + document.getElementById('countdownDate').value = `${year}-${month}-${day}`; + document.getElementById('countdownTime').value = `${hours}:${minutes}`; + } else { + // Clear fields if no target timestamp is set + document.getElementById('countdownDate').value = ''; + document.getElementById('countdownTime').value = ''; + } + // Countdown Label Input Validation + const countdownLabelInput = document.getElementById('countdownLabel'); + countdownLabelInput.addEventListener('input', function() { + // Convert to uppercase and remove any characters that are not A-Z or space + // Note: The `maxlength` attribute in HTML handles the length limit. + this.value = this.value.toUpperCase().replace(/[^A-Z0-9 :!'.\-]/g, ''); + }); + // Set initial value for countdownLabel from config.json (apply validation on load too) + if (data.countdown && data.countdown.label) { + countdownLabelInput.value = data.countdown.label.toUpperCase().replace(/[^A-Z0-9 :!'.\-]/g, ''); + } else { + countdownLabelInput.value = ''; + } + // Countdown Toggle Event Listener & Field Enabling + // If you're using onchange="setCountdownEnabled(this.checked)" directly in HTML, + // you would add setCountdownFieldsEnabled(this.checked) there as well. + // If you are using addEventListener, keep this: + countdownEnabledEl.addEventListener('change', function() { + setCountdownEnabled(this.checked); // Sends command to ESP + setCountdownFieldsEnabled(this.checked); // Enables/disables local fields + }); + const dramaticCountdownEl = document.getElementById('isDramaticCountdown'); + dramaticCountdownEl.addEventListener('change', function () { + setIsDramaticCountdown(this.checked); + }); + // Set initial state of fields when page loads + setCountdownFieldsEnabled(countdownEnabledEl.checked); + // Auto-detect browser's timezone if not set in config + if (!data.timeZone) { + try { + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + if ( + tz && + document.getElementById('timeZone').querySelector(`[value="${tz}"]`) + ) { + document.getElementById('timeZone').value = tz; + } else { document.getElementById('timeZone').value = ''; } - } else { - document.getElementById('timeZone').value = data.timeZone; + } catch (e) { + document.getElementById('timeZone').value = ''; } - }) - .catch(err => { - console.error('Failed to load config:', err); - showSavingModal(""); - updateSavingModal("⚠️ Failed to load configuration.", false); - - // Show appropriate button for load error - removeReloadButton(); - removeRestoreButton(); - const errorMsg = (err.message || "").toLowerCase(); - if ( - errorMsg.includes("config corrupted") || - errorMsg.includes("failed to write config") || - errorMsg.includes("restore") - ) { - ensureRestoreButton(); - } else { - ensureReloadButton(); - } - }); + } else { + document.getElementById('timeZone').value = data.timeZone; + } + }) + .catch(err => { + console.error('Failed to load config:', err); + showSavingModal(""); + updateSavingModal("⚠️ Failed to load configuration.", false); + // Show appropriate button for load error + removeReloadButton(); + removeRestoreButton(); + const errorMsg = (err.message || "").toLowerCase(); + if ( + errorMsg.includes("config corrupted") || + errorMsg.includes("failed to write config") || + errorMsg.includes("restore") + ) { + ensureRestoreButton(); + } else { + ensureReloadButton(); + } + }); document.querySelector('html').style.height = 'unset'; document.body.classList.add('loaded'); }; @@ -849,6 +838,15 @@ async function submitConfig(event) { formData.set('clockDuration', clockDuration); formData.set('weatherDuration', weatherDuration); + let apiKeyToSend = apiInput.value; + + // If the user left the masked key untouched, skip sending it + if (apiKeyToSend === MASK && hasSavedKey) { + formData.delete('openWeatherApiKey'); + } else { + formData.set('openWeatherApiKey', apiKeyToSend); + } + // Advanced: ensure correct values are set for advanced fields formData.set('brightness', document.getElementById('brightnessSlider').value); formData.set('flipDisplay', document.getElementById('flipDisplay').checked ? 'on' : ''); @@ -893,8 +891,6 @@ async function submitConfig(event) { params.append(pair[0], pair[1]); } - showSavingModal("Saving..."); - // Check AP mode status let isAPMode = false; try { @@ -906,6 +902,20 @@ async function submitConfig(event) { // Handle error appropriately (e.g., assume not in AP mode) } + if (isAPMode) { + showSavingModal(""); + updateSavingModal( + "✅ Settings saved successfully!

" + + "Rebooting the device now...

" + + "Your device will connect to your Wi-Fi.
" + + "Its new IP address will appear on the display for future access.", + true // show spinner + ); + } else{ + showSavingModal(""); + }; + + await new Promise(resolve => setTimeout(resolve, isAPMode ? 5000 : 0)); fetch('/save', { method: 'POST', body: params @@ -922,19 +932,23 @@ async function submitConfig(event) { isSaving = false; removeReloadButton(); removeRestoreButton(); - if (isAPMode) { - updateSavingModal("✅ Settings saved successfully!

Rebooting the device now... ", false); - setTimeout(() => { - document.getElementById('configForm').style.display = 'none'; - document.querySelector('.footer').style.display = 'none'; - document.querySelector('html').style.height = '100vh'; - document.body.style.height = '100vh'; - document.getElementById('configForm').style.display = 'none'; - updateSavingModal("✅ All done!
You can now close this tab safely.

Your device is now rebooting and connecting to your Wi-Fi. Its new IP address will appear on the display for future access.", false); - }, 5000); - return; - } else { + if (isAPMode) { + setTimeout(() => { + document.getElementById('configForm').style.display = 'none'; + document.querySelector('.footer').style.display = 'none'; + document.querySelector('html').style.height = '100vh'; + document.body.style.height = '100vh'; + updateSavingModal( + "✅ All done!
You can now close this tab safely.

" + + "Your device has rebooted and is now connected to your Wi-Fi.
" + + "Check the display for the current IP address.", + false // stop spinner + ); + }, 5000); + return; + } else { + showSavingModal(""); updateSavingModal("✅ Configuration saved successfully.

Device will reboot", false); setTimeout(() => location.href = location.href.split('#')[0], 3000); } @@ -948,7 +962,9 @@ async function submitConfig(event) { updateSavingModal("✅ Settings saved successfully!

Rebooting the device now... ", false); setTimeout(() => { document.getElementById('configForm').style.display = 'none'; - updateSavingModal("✅ All done!
You can now close this tab safely.

Your device is now rebooting and connecting to your Wi-Fi. Its new IP address will appear on the display for future access.", false); + updateSavingModal("✅ All done!
You can now close this tab safely.

" + + "Your device has rebooted and is now connected to your Wi-Fi.
" + + "Check the display for the current IP address.", false); }, 5000); removeReloadButton(); removeRestoreButton(); @@ -1055,32 +1071,31 @@ function removeRestoreButton() { let btn = document.getElementById('restoreButton'); if (btn && btn.parentNode) btn.parentNode.removeChild(btn); } - function restoreBackupConfig() { showSavingModal("Restoring backup..."); removeReloadButton(); removeRestoreButton(); fetch('/restore', { method: 'POST' }) - .then(response => { - if (!response.ok) { - throw new Error("Server returned an error"); - } - return response.json(); - }) - .then(data => { - updateSavingModal("✅ Backup restored! Device will now reboot."); - setTimeout(() => location.reload(), 1500); - }) - .catch(err => { - console.error("Restore error:", err); - updateSavingModal(`❌ Failed to restore backup: ${err.message}`, false); + .then(response => { + if (!response.ok) { + throw new Error("Server returned an error"); + } + return response.json(); + }) + .then(data => { + updateSavingModal("✅ Backup restored! Device will now reboot."); + setTimeout(() => location.reload(), 1500); + }) + .catch(err => { + console.error("Restore error:", err); + updateSavingModal(`❌ Failed to restore backup: ${err.message}`, false); - // Show only one button, for backup restore failures show reload. - removeReloadButton(); - removeRestoreButton(); - ensureReloadButton(); - }); + // Show only one button, for backup restore failures show reload. + removeReloadButton(); + removeRestoreButton(); + ensureReloadButton(); + }); } function hideSavingModal() { @@ -1092,28 +1107,28 @@ function hideSavingModal() { } const toggle = document.querySelector('.collapsible-toggle'); - const content = document.querySelector('.collapsible-content'); - toggle.addEventListener('click', function() { - const isOpen = toggle.classList.toggle('open'); - toggle.setAttribute('aria-expanded', isOpen); - content.setAttribute('aria-hidden', !isOpen); - if(isOpen) { - content.style.height = content.scrollHeight + 'px'; - content.addEventListener('transitionend', function handler() { - content.style.height = 'auto'; - content.removeEventListener('transitionend', handler); - }); - } else { - content.style.height = content.scrollHeight + 'px'; - // Force reflow to make sure the browser notices the height before transitioning to 0 - void content.offsetHeight; - content.style.height = '0px'; - } - }); - // Optional: If open on load, set height to auto - if(toggle.classList.contains('open')) { +const content = document.querySelector('.collapsible-content'); +toggle.addEventListener('click', function() { + const isOpen = toggle.classList.toggle('open'); + toggle.setAttribute('aria-expanded', isOpen); + content.setAttribute('aria-hidden', !isOpen); + if(isOpen) { + content.style.height = content.scrollHeight + 'px'; + content.addEventListener('transitionend', function handler() { content.style.height = 'auto'; - } + content.removeEventListener('transitionend', handler); + }); + } else { + content.style.height = content.scrollHeight + 'px'; + // Force reflow to make sure the browser notices the height before transitioning to 0 + void content.offsetHeight; + content.style.height = '0px'; + } +}); +// Optional: If open on load, set height to auto +if(toggle.classList.contains('open')) { + content.style.height = 'auto'; +} let brightnessDebounceTimeout = null; @@ -1217,21 +1232,20 @@ function setCountdownFieldsEnabled(enabled) { // Existing function to send countdown enable/disable command to ESP function setCountdownEnabled(val) { - fetch('/set_countdown_enabled', { - method: 'POST', - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false - }); + fetch('/set_countdown_enabled', { + method: 'POST', + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false + }); } function setIsDramaticCountdown(val) { - fetch('/set_dramatic_countdown', { - method: 'POST', - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false - }); + fetch('/set_dramatic_countdown', { + method: 'POST', + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false + }); } - // --- END Countdown Controls Logic --- @@ -1244,37 +1258,82 @@ function setDimmingFieldsEnabled(enabled) { function getLocation() { fetch('http://ip-api.com/json/') - .then(response => response.json()) - .then(data => { - // Update your latitude/longitude fields - document.getElementById('openWeatherCity').value = data.lat; - document.getElementById('openWeatherCountry').value = data.lon; + .then(response => response.json()) + .then(data => { + // Update your latitude/longitude fields + document.getElementById('openWeatherCity').value = data.lat; + document.getElementById('openWeatherCountry').value = data.lon; - // Determine the label to show on the button - const button = document.getElementById('geo-button'); - let label = data.city; - if (!label) label = data.regionName; - if (!label) label = data.country; - if (!label) label = "Location Found"; + // Determine the label to show on the button + const button = document.getElementById('geo-button'); + let label = data.city; + if (!label) label = data.regionName; + if (!label) label = data.country; + if (!label) label = "Location Found"; - button.textContent = "Location: " + label; - button.disabled = true; - button.classList.add('geo-disabled'); + button.textContent = "Location: " + label; + button.disabled = true; + button.classList.add('geo-disabled'); - console.log("Location fetched via ip-api.com. Free service: http://ip-api.com/"); - }) - .catch(error => { - alert( - "Failed to guess your location.\n\n" + - "This may happen if:\n" + - "- You are using an AdBlocker\n" + - "- There is a network issue\n" + - "- The service might be temporarily down.\n\n" + - "You can visit https://openweathermap.org/find to manually search for your city and get latitude/longitude." - ); - }); + console.log("Location fetched via ip-api.com. Free service: http://ip-api.com/"); + }) + .catch(error => { + alert( + "Failed to guess your location.\n\n" + + "This may happen if:\n" + + "- You are using an AdBlocker\n" + + "- There is a network issue\n" + + "- The service might be temporarily down.\n\n" + + "You can visit https://openweathermap.org/find to manually search for your city and get latitude/longitude." + ); + }); } + +// --- OpenWeather API Key field UX --- +const MASK_LENGTH = 32; +const MASK = '*'.repeat(MASK_LENGTH); +const apiInput = document.getElementById('openWeatherApiKey'); +let hasSavedKey = false; + +// --- Initialize the field after config load --- +if (apiInput.value && apiInput.value.trim() !== '') { + apiInput.value = MASK; // show mask + hasSavedKey = true; +} else { + apiInput.value = ''; + hasSavedKey = false; +} + +// --- Detect user clearing intent --- +apiInput.addEventListener('input', () => { + apiInput.dataset.clearing = apiInput.value === '' ? 'true' : 'false'; +}); + +// --- Handle Delete/Backspace when focused but empty --- +apiInput.addEventListener('keydown', (e) => { + if ((e.key === 'Backspace' || e.key === 'Delete') && apiInput.value === '') { + apiInput.dataset.clearing = 'true'; + } +}); + +// --- Focus handler: clear mask for editing --- +apiInput.addEventListener('focus', () => { + if (apiInput.value === MASK) apiInput.value = ''; +}); + +// --- Blur handler: restore mask if user didn’t clear the field --- +apiInput.addEventListener('blur', () => { + if (apiInput.value === '') { + if (hasSavedKey && apiInput.dataset.clearing !== 'true') { + apiInput.value = MASK; // remask + } else { + hasSavedKey = false; // user cleared the key + apiInput.dataset.clearing = 'false'; + apiInput.value = ''; // leave blank + } + } +}); \ No newline at end of file diff --git a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino index fe63b6e..b9be6ba 100644 --- a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino +++ b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino @@ -20,9 +20,9 @@ #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 -#define CLK_PIN 12 -#define DATA_PIN 15 -#define CS_PIN 13 +#define CLK_PIN 14 //D5 +#define CS_PIN 13 //D7 +#define DATA_PIN 15 //D8 MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES); AsyncWebServer server(80); @@ -137,7 +137,7 @@ const unsigned long descriptionDuration = 3000; // 3s for short text static unsigned long descScrollEndTime = 0; // for post-scroll delay (re-used for scroll timing) const unsigned long descriptionScrollPause = 300; // 300ms pause after scroll -// --- Safe WiFi credential getters --- +// --- Safe WiFi credential and API getters --- const char *getSafeSsid() { return isAPMode ? "" : ssid; } @@ -150,6 +150,13 @@ const char *getSafePassword() { } } +const char *getSafeApiKey() { + if (strlen(openWeatherApiKey) == 0) { + return ""; + } else { + return "********************************"; // Always masked, even in AP mode + } +} // Scroll flipped textEffect_t getEffectiveScrollDirection(textEffect_t desiredDirection, bool isFlipped) { @@ -600,6 +607,7 @@ void setupWebServer() { // Always sanitize before sending to browser doc[F("ssid")] = getSafeSsid(); doc[F("password")] = getSafePassword(); + doc[F("openWeatherApiKey")] = getSafeApiKey(); doc[F("mode")] = isAPMode ? "ap" : "sta"; String response; @@ -656,7 +664,15 @@ void setupWebServer() { } } - else { + else if (n == "openWeatherApiKey") { + if (v != "********************************") { // ignore mask only + doc[n] = v; // save new key (even if empty) + Serial.print(F("[SAVE] API key updated: ")); + Serial.println(v.length() == 0 ? "(empty)" : v); + } else { + Serial.println(F("[SAVE] API key unchanged (mask ignored).")); + } + } else { doc[n] = v; } } @@ -1060,7 +1076,10 @@ void setupWebServer() { request->send(200, "application/json", "{\"ok\":true}"); }); - + server.on("/generate_204", HTTP_GET, handleCaptivePortal); // Android + server.on("/fwlink", HTTP_GET, handleCaptivePortal); // Windows + server.on("/hotspot-detect.html", HTTP_GET, handleCaptivePortal); // iOS/macOS + server.onNotFound(handleCaptivePortal); server.begin(); Serial.println(F("[WEBSERVER] Web server started")); } diff --git a/ESPTimeCast_ESP8266/data/index.html b/ESPTimeCast_ESP8266/data/index.html index d25eca5..d6b14d9 100644 --- a/ESPTimeCast_ESP8266/data/index.html +++ b/ESPTimeCast_ESP8266/data/index.html @@ -679,160 +679,149 @@ window.onbeforeunload = function () { window.onload = function () { fetch('/config.json') - .then(response => response.json()) - .then(data => { - isAPMode = (data.mode === "ap"); - if (isAPMode) { - document.querySelector('.geo-note').style.display = 'block'; - document.getElementById('geo-button').classList.add('geo-disabled'); - document.getElementById('geo-button').disabled = true; - } + .then(response => response.json()) + .then(data => { + isAPMode = (data.mode === "ap"); + if (isAPMode) { + document.querySelector('.geo-note').style.display = 'block'; + document.getElementById('geo-button').classList.add('geo-disabled'); + document.getElementById('geo-button').disabled = true; + } + document.getElementById('ssid').value = data.ssid || ''; + document.getElementById('password').value = data.password || ''; + const apiInput = document.getElementById('openWeatherApiKey'); + if (data.openWeatherApiKey && data.openWeatherApiKey.trim() !== '') { + apiInput.value = MASK; + hasSavedKey = true; // mark it as having a saved key + } else { + apiInput.value = ''; + hasSavedKey = false; + } + document.getElementById('openWeatherCity').value = data.openWeatherCity || ''; + document.getElementById('openWeatherCountry').value = data.openWeatherCountry || ''; + document.getElementById('weatherUnits').checked = (data.weatherUnits === "imperial"); + document.getElementById('clockDuration').value = (data.clockDuration || 10000) / 1000; + document.getElementById('weatherDuration').value = (data.weatherDuration || 5000) / 1000; + document.getElementById('language').value = data.language || ''; - document.getElementById('ssid').value = data.ssid || ''; - document.getElementById('password').value = data.password || ''; - document.getElementById('openWeatherApiKey').value = data.openWeatherApiKey || ''; - document.getElementById('openWeatherCity').value = data.openWeatherCity || ''; - document.getElementById('openWeatherCountry').value = data.openWeatherCountry || ''; - document.getElementById('weatherUnits').checked = (data.weatherUnits === "imperial"); - document.getElementById('clockDuration').value = (data.clockDuration || 10000) / 1000; - document.getElementById('weatherDuration').value = (data.weatherDuration || 5000) / 1000; - document.getElementById('language').value = data.language || ''; - // Advanced: - document.getElementById('brightnessSlider').value = typeof data.brightness !== "undefined" ? data.brightness : 10; - document.getElementById('brightnessValue').textContent = (document.getElementById('brightnessSlider').value == -1 ? 'Off' : document.getElementById('brightnessSlider').value); - document.getElementById('flipDisplay').checked = !!data.flipDisplay; - document.getElementById('ntpServer1').value = data.ntpServer1 || ""; - document.getElementById('ntpServer2').value = data.ntpServer2 || ""; - document.getElementById('twelveHourToggle').checked = !!data.twelveHourToggle; - document.getElementById('showDayOfWeek').checked = !!data.showDayOfWeek; - document.getElementById('showDate').checked = !!data.showDate; - document.getElementById('showHumidity').checked = !!data.showHumidity; - document.getElementById('colonBlinkEnabled').checked = !!data.colonBlinkEnabled; - document.getElementById('showWeatherDescription').checked = !!data.showWeatherDescription; - // Dimming controls -const dimmingEnabledEl = document.getElementById('dimmingEnabled'); -const isDimming = (data.dimmingEnabled === true || data.dimmingEnabled === "true" || data.dimmingEnabled === 1); -dimmingEnabledEl.checked = isDimming; + // Advanced: + document.getElementById('brightnessSlider').value = typeof data.brightness !== "undefined" ? data.brightness : 10; + document.getElementById('brightnessValue').textContent = (document.getElementById('brightnessSlider').value == -1 ? 'Off' : document.getElementById('brightnessSlider').value); + document.getElementById('flipDisplay').checked = !!data.flipDisplay; + document.getElementById('ntpServer1').value = data.ntpServer1 || ""; + document.getElementById('ntpServer2').value = data.ntpServer2 || ""; + document.getElementById('twelveHourToggle').checked = !!data.twelveHourToggle; + document.getElementById('showDayOfWeek').checked = !!data.showDayOfWeek; + document.getElementById('showDate').checked = !!data.showDate; + document.getElementById('showHumidity').checked = !!data.showHumidity; + document.getElementById('colonBlinkEnabled').checked = !!data.colonBlinkEnabled; + document.getElementById('showWeatherDescription').checked = !!data.showWeatherDescription; -// Defer field enabling until checkbox state is rendered -setTimeout(() => { - setDimmingFieldsEnabled(dimmingEnabledEl.checked); -}, 0); - -dimmingEnabledEl.addEventListener('change', function () { - setDimmingFieldsEnabled(this.checked); -}); - - document.getElementById('dimStartTime').value = - (data.dimStartHour !== undefined ? String(data.dimStartHour).padStart(2, '0') : "18") + ":" + - (data.dimStartMinute !== undefined ? String(data.dimStartMinute).padStart(2, '0') : "00"); - -document.getElementById('dimEndTime').value = - (data.dimEndHour !== undefined ? String(data.dimEndHour).padStart(2, '0') : "08") + ":" + - (data.dimEndMinute !== undefined ? String(data.dimEndMinute).padStart(2, '0') : "00"); - - document.getElementById('dimBrightness').value = (data.dimBrightness !== undefined ? data.dimBrightness : 2); - // Then update the span's text content with that value - document.getElementById('dimmingBrightnessValue').textContent = (document.getElementById('dimBrightness').value == -1 ? 'Off' : document.getElementById('dimBrightness').value); - - setDimmingFieldsEnabled(!!data.dimmingEnabled); - - // --- NEW: Populate Countdown Fields --- - document.getElementById('isDramaticCountdown').checked = !!(data.countdown && data.countdown.isDramaticCountdown); - const countdownEnabledEl = document.getElementById('countdownEnabled'); // Get reference - countdownEnabledEl.checked = !!(data.countdown && data.countdown.enabled); - - if (data.countdown && data.countdown.targetTimestamp) { - // Convert Unix timestamp (seconds) to milliseconds for JavaScript Date object - const targetDate = new Date(data.countdown.targetTimestamp * 1000); - const year = targetDate.getFullYear(); - // Month is 0-indexed in JS, so add 1 - const month = (targetDate.getMonth() + 1).toString().padStart(2, '0'); - const day = targetDate.getDate().toString().padStart(2, '0'); - const hours = targetDate.getHours().toString().padStart(2, '0'); - const minutes = targetDate.getMinutes().toString().padStart(2, '0'); - - document.getElementById('countdownDate').value = `${year}-${month}-${day}`; - document.getElementById('countdownTime').value = `${hours}:${minutes}`; - } else { - // Clear fields if no target timestamp is set - document.getElementById('countdownDate').value = ''; - document.getElementById('countdownTime').value = ''; - } - // --- END NEW --- - - // --- NEW: Countdown Label Input Validation --- - const countdownLabelInput = document.getElementById('countdownLabel'); - countdownLabelInput.addEventListener('input', function() { - // Convert to uppercase and remove any characters that are not A-Z or space - // Note: The `maxlength` attribute in HTML handles the length limit. - this.value = this.value.toUpperCase().replace(/[^A-Z0-9 :!'.\-]/g, ''); - }); - // Set initial value for countdownLabel from config.json (apply validation on load too) - if (data.countdown && data.countdown.label) { - countdownLabelInput.value = data.countdown.label.toUpperCase().replace(/[^A-Z0-9 :!'.\-]/g, ''); - } else { - countdownLabelInput.value = ''; - } - // --- END NEW --- - - - // --- NEW: Countdown Toggle Event Listener & Field Enabling --- - // If you're using onchange="setCountdownEnabled(this.checked)" directly in HTML, - // you would add setCountdownFieldsEnabled(this.checked) there as well. - // If you are using addEventListener, keep this: - countdownEnabledEl.addEventListener('change', function() { - setCountdownEnabled(this.checked); // Sends command to ESP - setCountdownFieldsEnabled(this.checked); // Enables/disables local fields - }); - -const dramaticCountdownEl = document.getElementById('isDramaticCountdown'); -dramaticCountdownEl.addEventListener('change', function () { - setIsDramaticCountdown(this.checked); -}); - - // Set initial state of fields when page loads - setCountdownFieldsEnabled(countdownEnabledEl.checked); - // --- END NEW --- - - // Auto-detect browser's timezone if not set in config - if (!data.timeZone) { - try { - const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; - if ( - tz && - document.getElementById('timeZone').querySelector(`[value="${tz}"]`) - ) { - document.getElementById('timeZone').value = tz; - } else { - document.getElementById('timeZone').value = ''; - } - } catch (e) { + // Dimming controls + const dimmingEnabledEl = document.getElementById('dimmingEnabled'); + const isDimming = (data.dimmingEnabled === true || data.dimmingEnabled === "true" || data.dimmingEnabled === 1); + dimmingEnabledEl.checked = isDimming; + // Defer field enabling until checkbox state is rendered + setTimeout(() => { + setDimmingFieldsEnabled(dimmingEnabledEl.checked); + }, 0); + dimmingEnabledEl.addEventListener('change', function () { + setDimmingFieldsEnabled(this.checked); + }); + document.getElementById('dimStartTime').value = + (data.dimStartHour !== undefined ? String(data.dimStartHour).padStart(2, '0') : "18") + ":" + + (data.dimStartMinute !== undefined ? String(data.dimStartMinute).padStart(2, '0') : "00"); + document.getElementById('dimEndTime').value = + (data.dimEndHour !== undefined ? String(data.dimEndHour).padStart(2, '0') : "08") + ":" + + (data.dimEndMinute !== undefined ? String(data.dimEndMinute).padStart(2, '0') : "00"); + document.getElementById('dimBrightness').value = (data.dimBrightness !== undefined ? data.dimBrightness : 2); + // Then update the span's text content with that value + document.getElementById('dimmingBrightnessValue').textContent = (document.getElementById('dimBrightness').value == -1 ? 'Off' : document.getElementById('dimBrightness').value); + setDimmingFieldsEnabled(!!data.dimmingEnabled); + // --- Populate Countdown Fields --- + document.getElementById('isDramaticCountdown').checked = !!(data.countdown && data.countdown.isDramaticCountdown); + const countdownEnabledEl = document.getElementById('countdownEnabled'); // Get reference + countdownEnabledEl.checked = !!(data.countdown && data.countdown.enabled); + if (data.countdown && data.countdown.targetTimestamp) { + // Convert Unix timestamp (seconds) to milliseconds for JavaScript Date object + const targetDate = new Date(data.countdown.targetTimestamp * 1000); + const year = targetDate.getFullYear(); + // Month is 0-indexed in JS, so add 1 + const month = (targetDate.getMonth() + 1).toString().padStart(2, '0'); + const day = targetDate.getDate().toString().padStart(2, '0'); + const hours = targetDate.getHours().toString().padStart(2, '0'); + const minutes = targetDate.getMinutes().toString().padStart(2, '0'); + document.getElementById('countdownDate').value = `${year}-${month}-${day}`; + document.getElementById('countdownTime').value = `${hours}:${minutes}`; + } else { + // Clear fields if no target timestamp is set + document.getElementById('countdownDate').value = ''; + document.getElementById('countdownTime').value = ''; + } + // Countdown Label Input Validation + const countdownLabelInput = document.getElementById('countdownLabel'); + countdownLabelInput.addEventListener('input', function() { + // Convert to uppercase and remove any characters that are not A-Z or space + // Note: The `maxlength` attribute in HTML handles the length limit. + this.value = this.value.toUpperCase().replace(/[^A-Z0-9 :!'.\-]/g, ''); + }); + // Set initial value for countdownLabel from config.json (apply validation on load too) + if (data.countdown && data.countdown.label) { + countdownLabelInput.value = data.countdown.label.toUpperCase().replace(/[^A-Z0-9 :!'.\-]/g, ''); + } else { + countdownLabelInput.value = ''; + } + // Countdown Toggle Event Listener & Field Enabling + // If you're using onchange="setCountdownEnabled(this.checked)" directly in HTML, + // you would add setCountdownFieldsEnabled(this.checked) there as well. + // If you are using addEventListener, keep this: + countdownEnabledEl.addEventListener('change', function() { + setCountdownEnabled(this.checked); // Sends command to ESP + setCountdownFieldsEnabled(this.checked); // Enables/disables local fields + }); + const dramaticCountdownEl = document.getElementById('isDramaticCountdown'); + dramaticCountdownEl.addEventListener('change', function () { + setIsDramaticCountdown(this.checked); + }); + // Set initial state of fields when page loads + setCountdownFieldsEnabled(countdownEnabledEl.checked); + // Auto-detect browser's timezone if not set in config + if (!data.timeZone) { + try { + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + if ( + tz && + document.getElementById('timeZone').querySelector(`[value="${tz}"]`) + ) { + document.getElementById('timeZone').value = tz; + } else { document.getElementById('timeZone').value = ''; } - } else { - document.getElementById('timeZone').value = data.timeZone; + } catch (e) { + document.getElementById('timeZone').value = ''; } - }) - .catch(err => { - console.error('Failed to load config:', err); - showSavingModal(""); - updateSavingModal("⚠️ Failed to load configuration.", false); - - // Show appropriate button for load error - removeReloadButton(); - removeRestoreButton(); - const errorMsg = (err.message || "").toLowerCase(); - if ( - errorMsg.includes("config corrupted") || - errorMsg.includes("failed to write config") || - errorMsg.includes("restore") - ) { - ensureRestoreButton(); - } else { - ensureReloadButton(); - } - }); + } else { + document.getElementById('timeZone').value = data.timeZone; + } + }) + .catch(err => { + console.error('Failed to load config:', err); + showSavingModal(""); + updateSavingModal("⚠️ Failed to load configuration.", false); + // Show appropriate button for load error + removeReloadButton(); + removeRestoreButton(); + const errorMsg = (err.message || "").toLowerCase(); + if ( + errorMsg.includes("config corrupted") || + errorMsg.includes("failed to write config") || + errorMsg.includes("restore") + ) { + ensureRestoreButton(); + } else { + ensureReloadButton(); + } + }); document.querySelector('html').style.height = 'unset'; document.body.classList.add('loaded'); }; @@ -849,6 +838,15 @@ async function submitConfig(event) { formData.set('clockDuration', clockDuration); formData.set('weatherDuration', weatherDuration); + let apiKeyToSend = apiInput.value; + + // If the user left the masked key untouched, skip sending it + if (apiKeyToSend === MASK && hasSavedKey) { + formData.delete('openWeatherApiKey'); + } else { + formData.set('openWeatherApiKey', apiKeyToSend); + } + // Advanced: ensure correct values are set for advanced fields formData.set('brightness', document.getElementById('brightnessSlider').value); formData.set('flipDisplay', document.getElementById('flipDisplay').checked ? 'on' : ''); @@ -893,8 +891,6 @@ async function submitConfig(event) { params.append(pair[0], pair[1]); } - showSavingModal("Saving..."); - // Check AP mode status let isAPMode = false; try { @@ -906,6 +902,20 @@ async function submitConfig(event) { // Handle error appropriately (e.g., assume not in AP mode) } + if (isAPMode) { + showSavingModal(""); + updateSavingModal( + "✅ Settings saved successfully!

" + + "Rebooting the device now...

" + + "Your device will connect to your Wi-Fi.
" + + "Its new IP address will appear on the display for future access.", + true // show spinner + ); + } else{ + showSavingModal(""); + }; + + await new Promise(resolve => setTimeout(resolve, isAPMode ? 5000 : 0)); fetch('/save', { method: 'POST', body: params @@ -922,19 +932,23 @@ async function submitConfig(event) { isSaving = false; removeReloadButton(); removeRestoreButton(); - if (isAPMode) { - updateSavingModal("✅ Settings saved successfully!

Rebooting the device now... ", false); - setTimeout(() => { - document.getElementById('configForm').style.display = 'none'; - document.querySelector('.footer').style.display = 'none'; - document.querySelector('html').style.height = '100vh'; - document.body.style.height = '100vh'; - document.getElementById('configForm').style.display = 'none'; - updateSavingModal("✅ All done!
You can now close this tab safely.

Your device is now rebooting and connecting to your Wi-Fi. Its new IP address will appear on the display for future access.", false); - }, 5000); - return; - } else { + if (isAPMode) { + setTimeout(() => { + document.getElementById('configForm').style.display = 'none'; + document.querySelector('.footer').style.display = 'none'; + document.querySelector('html').style.height = '100vh'; + document.body.style.height = '100vh'; + updateSavingModal( + "✅ All done!
You can now close this tab safely.

" + + "Your device has rebooted and is now connected to your Wi-Fi.
" + + "Check the display for the current IP address.", + false // stop spinner + ); + }, 5000); + return; + } else { + showSavingModal(""); updateSavingModal("✅ Configuration saved successfully.

Device will reboot", false); setTimeout(() => location.href = location.href.split('#')[0], 3000); } @@ -948,7 +962,9 @@ async function submitConfig(event) { updateSavingModal("✅ Settings saved successfully!

Rebooting the device now... ", false); setTimeout(() => { document.getElementById('configForm').style.display = 'none'; - updateSavingModal("✅ All done!
You can now close this tab safely.

Your device is now rebooting and connecting to your Wi-Fi. Its new IP address will appear on the display for future access.", false); + updateSavingModal("✅ All done!
You can now close this tab safely.

" + + "Your device has rebooted and is now connected to your Wi-Fi.
" + + "Check the display for the current IP address.", false); }, 5000); removeReloadButton(); removeRestoreButton(); @@ -1055,32 +1071,31 @@ function removeRestoreButton() { let btn = document.getElementById('restoreButton'); if (btn && btn.parentNode) btn.parentNode.removeChild(btn); } - function restoreBackupConfig() { showSavingModal("Restoring backup..."); removeReloadButton(); removeRestoreButton(); fetch('/restore', { method: 'POST' }) - .then(response => { - if (!response.ok) { - throw new Error("Server returned an error"); - } - return response.json(); - }) - .then(data => { - updateSavingModal("✅ Backup restored! Device will now reboot."); - setTimeout(() => location.reload(), 1500); - }) - .catch(err => { - console.error("Restore error:", err); - updateSavingModal(`❌ Failed to restore backup: ${err.message}`, false); + .then(response => { + if (!response.ok) { + throw new Error("Server returned an error"); + } + return response.json(); + }) + .then(data => { + updateSavingModal("✅ Backup restored! Device will now reboot."); + setTimeout(() => location.reload(), 1500); + }) + .catch(err => { + console.error("Restore error:", err); + updateSavingModal(`❌ Failed to restore backup: ${err.message}`, false); - // Show only one button, for backup restore failures show reload. - removeReloadButton(); - removeRestoreButton(); - ensureReloadButton(); - }); + // Show only one button, for backup restore failures show reload. + removeReloadButton(); + removeRestoreButton(); + ensureReloadButton(); + }); } function hideSavingModal() { @@ -1092,28 +1107,28 @@ function hideSavingModal() { } const toggle = document.querySelector('.collapsible-toggle'); - const content = document.querySelector('.collapsible-content'); - toggle.addEventListener('click', function() { - const isOpen = toggle.classList.toggle('open'); - toggle.setAttribute('aria-expanded', isOpen); - content.setAttribute('aria-hidden', !isOpen); - if(isOpen) { - content.style.height = content.scrollHeight + 'px'; - content.addEventListener('transitionend', function handler() { - content.style.height = 'auto'; - content.removeEventListener('transitionend', handler); - }); - } else { - content.style.height = content.scrollHeight + 'px'; - // Force reflow to make sure the browser notices the height before transitioning to 0 - void content.offsetHeight; - content.style.height = '0px'; - } - }); - // Optional: If open on load, set height to auto - if(toggle.classList.contains('open')) { +const content = document.querySelector('.collapsible-content'); +toggle.addEventListener('click', function() { + const isOpen = toggle.classList.toggle('open'); + toggle.setAttribute('aria-expanded', isOpen); + content.setAttribute('aria-hidden', !isOpen); + if(isOpen) { + content.style.height = content.scrollHeight + 'px'; + content.addEventListener('transitionend', function handler() { content.style.height = 'auto'; - } + content.removeEventListener('transitionend', handler); + }); + } else { + content.style.height = content.scrollHeight + 'px'; + // Force reflow to make sure the browser notices the height before transitioning to 0 + void content.offsetHeight; + content.style.height = '0px'; + } +}); +// Optional: If open on load, set height to auto +if(toggle.classList.contains('open')) { + content.style.height = 'auto'; +} let brightnessDebounceTimeout = null; @@ -1217,21 +1232,20 @@ function setCountdownFieldsEnabled(enabled) { // Existing function to send countdown enable/disable command to ESP function setCountdownEnabled(val) { - fetch('/set_countdown_enabled', { - method: 'POST', - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false - }); + fetch('/set_countdown_enabled', { + method: 'POST', + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false + }); } function setIsDramaticCountdown(val) { - fetch('/set_dramatic_countdown', { - method: 'POST', - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false - }); + fetch('/set_dramatic_countdown', { + method: 'POST', + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false + }); } - // --- END Countdown Controls Logic --- @@ -1244,37 +1258,82 @@ function setDimmingFieldsEnabled(enabled) { function getLocation() { fetch('http://ip-api.com/json/') - .then(response => response.json()) - .then(data => { - // Update your latitude/longitude fields - document.getElementById('openWeatherCity').value = data.lat; - document.getElementById('openWeatherCountry').value = data.lon; + .then(response => response.json()) + .then(data => { + // Update your latitude/longitude fields + document.getElementById('openWeatherCity').value = data.lat; + document.getElementById('openWeatherCountry').value = data.lon; - // Determine the label to show on the button - const button = document.getElementById('geo-button'); - let label = data.city; - if (!label) label = data.regionName; - if (!label) label = data.country; - if (!label) label = "Location Found"; + // Determine the label to show on the button + const button = document.getElementById('geo-button'); + let label = data.city; + if (!label) label = data.regionName; + if (!label) label = data.country; + if (!label) label = "Location Found"; - button.textContent = "Location: " + label; - button.disabled = true; - button.classList.add('geo-disabled'); + button.textContent = "Location: " + label; + button.disabled = true; + button.classList.add('geo-disabled'); - console.log("Location fetched via ip-api.com. Free service: http://ip-api.com/"); - }) - .catch(error => { - alert( - "Failed to guess your location.\n\n" + - "This may happen if:\n" + - "- You are using an AdBlocker\n" + - "- There is a network issue\n" + - "- The service might be temporarily down.\n\n" + - "You can visit https://openweathermap.org/find to manually search for your city and get latitude/longitude." - ); - }); + console.log("Location fetched via ip-api.com. Free service: http://ip-api.com/"); + }) + .catch(error => { + alert( + "Failed to guess your location.\n\n" + + "This may happen if:\n" + + "- You are using an AdBlocker\n" + + "- There is a network issue\n" + + "- The service might be temporarily down.\n\n" + + "You can visit https://openweathermap.org/find to manually search for your city and get latitude/longitude." + ); + }); } + +// --- OpenWeather API Key field UX --- +const MASK_LENGTH = 32; +const MASK = '*'.repeat(MASK_LENGTH); +const apiInput = document.getElementById('openWeatherApiKey'); +let hasSavedKey = false; + +// --- Initialize the field after config load --- +if (apiInput.value && apiInput.value.trim() !== '') { + apiInput.value = MASK; // show mask + hasSavedKey = true; +} else { + apiInput.value = ''; + hasSavedKey = false; +} + +// --- Detect user clearing intent --- +apiInput.addEventListener('input', () => { + apiInput.dataset.clearing = apiInput.value === '' ? 'true' : 'false'; +}); + +// --- Handle Delete/Backspace when focused but empty --- +apiInput.addEventListener('keydown', (e) => { + if ((e.key === 'Backspace' || e.key === 'Delete') && apiInput.value === '') { + apiInput.dataset.clearing = 'true'; + } +}); + +// --- Focus handler: clear mask for editing --- +apiInput.addEventListener('focus', () => { + if (apiInput.value === MASK) apiInput.value = ''; +}); + +// --- Blur handler: restore mask if user didn’t clear the field --- +apiInput.addEventListener('blur', () => { + if (apiInput.value === '') { + if (hasSavedKey && apiInput.dataset.clearing !== 'true') { + apiInput.value = MASK; // remask + } else { + hasSavedKey = false; // user cleared the key + apiInput.dataset.clearing = 'false'; + apiInput.value = ''; // leave blank + } + } +}); \ No newline at end of file diff --git a/assets/wiring3.png b/assets/wiring3.png new file mode 100644 index 0000000000000000000000000000000000000000..63287e1a09148f2454dcd2a86ab1315ad5fbf474 GIT binary patch literal 75893 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 z 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 literal 0 HcmV?d00001