diff --git a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino index fc87c63..c7742ba 100644 --- a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino +++ b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino @@ -147,7 +147,7 @@ const unsigned long descriptionScrollPause = 300; // 300ms pause after scroll // --- Safe WiFi credential and API getters --- const char *getSafeSsid() { - return isAPMode ? "" : ssid; + return isAPMode ? "********" : ssid; } const char *getSafePassword() { @@ -1076,7 +1076,6 @@ void setupWebServer() { request->send(200, "application/json", "{\"ok\":true}"); }); - server.on("/set_dramatic_countdown", HTTP_POST, [](AsyncWebServerRequest *request) { bool enableDramaticNow = false; if (request->hasParam("value", true)) { @@ -1122,6 +1121,168 @@ void setupWebServer() { request->send(200, "text/plain", formatted); }); + server.on("/export", HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println(F("[WEBSERVER] Request: /export")); + + File f; + if (LittleFS.exists("/config.json")) { + f = LittleFS.open("/config.json", "r"); + Serial.println(F("[EXPORT] Using /config.json")); + } else if (LittleFS.exists("/config.bak")) { + f = LittleFS.open("/config.bak", "r"); + Serial.println(F("[EXPORT] /config.json not found, using /config.bak")); + } else { + request->send(404, "application/json", "{\"error\":\"No config found\"}"); + return; + } + + DynamicJsonDocument doc(2048); + DeserializationError err = deserializeJson(doc, f); + f.close(); + if (err) { + Serial.print(F("[EXPORT] Error parsing config: ")); + Serial.println(err.f_str()); + request->send(500, "application/json", "{\"error\":\"Failed to parse config\"}"); + return; + } + + // Only sanitize if NOT in AP mode + if (!isAPMode) { + doc["ssid"] = "********"; + doc["password"] = "********"; + doc["openWeatherApiKey"] = "********************************"; + } + + doc["mode"] = isAPMode ? "ap" : "sta"; + + String jsonOut; + serializeJsonPretty(doc, jsonOut); + + AsyncWebServerResponse *resp = request->beginResponse(200, "application/json", jsonOut); + resp->addHeader("Content-Disposition", "attachment; filename=\"config.json\""); + request->send(resp); + }); + + server.on("/upload", HTTP_GET, [](AsyncWebServerRequest *request) { + String html = R"rawliteral( + + + + + + + + +

Upload config.json

+
+ +
+ + + )rawliteral"; + request->send(200, "text/html", html); + }); + + server.on( + "/upload", HTTP_POST, [](AsyncWebServerRequest *request) { + String html = R"rawliteral( + + + + + + Upload Successful + + + + +

File uploaded successfully!

+

Returning to main page...

+ + + )rawliteral"; + request->send(200, "text/html", html); + // Restart after short delay to let browser handle redirect + request->onDisconnect([]() { + delay(500); // ensure response is sent + ESP.restart(); + }); + }, + [](AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) { + static File f; + if (index == 0) { + f = LittleFS.open("/config.json", "w"); // start new file + } + if (f) f.write(data, len); // write chunk + if (final) f.close(); // finish file + }); + 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 diff --git a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino index d2f4c4b..81870b7 100644 --- a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino +++ b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino @@ -148,7 +148,7 @@ const unsigned long descriptionScrollPause = 300; // 300ms pause after scroll // --- Safe WiFi credential and API getters --- const char *getSafeSsid() { - return isAPMode ? "" : ssid; + return isAPMode ? "********" : ssid; } const char *getSafePassword() { @@ -1124,6 +1124,168 @@ void setupWebServer() { request->send(200, "text/plain", formatted); }); + server.on("/export", HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println(F("[WEBSERVER] Request: /export")); + + File f; + if (LittleFS.exists("/config.json")) { + f = LittleFS.open("/config.json", "r"); + Serial.println(F("[EXPORT] Using /config.json")); + } else if (LittleFS.exists("/config.bak")) { + f = LittleFS.open("/config.bak", "r"); + Serial.println(F("[EXPORT] /config.json not found, using /config.bak")); + } else { + request->send(404, "application/json", "{\"error\":\"No config found\"}"); + return; + } + + DynamicJsonDocument doc(2048); + DeserializationError err = deserializeJson(doc, f); + f.close(); + if (err) { + Serial.print(F("[EXPORT] Error parsing config: ")); + Serial.println(err.f_str()); + request->send(500, "application/json", "{\"error\":\"Failed to parse config\"}"); + return; + } + + // Only sanitize if NOT in AP mode + if (!isAPMode) { + doc["ssid"] = "********"; + doc["password"] = "********"; + doc["openWeatherApiKey"] = "********************************"; + } + + doc["mode"] = isAPMode ? "ap" : "sta"; + + String jsonOut; + serializeJsonPretty(doc, jsonOut); + + AsyncWebServerResponse *resp = request->beginResponse(200, "application/json", jsonOut); + resp->addHeader("Content-Disposition", "attachment; filename=\"config.json\""); + request->send(resp); + }); + + server.on("/upload", HTTP_GET, [](AsyncWebServerRequest *request) { + String html = R"rawliteral( + + + + + + + + +

Upload config.json

+
+ +
+ + + )rawliteral"; + request->send(200, "text/html", html); + }); + + server.on( + "/upload", HTTP_POST, [](AsyncWebServerRequest *request) { + String html = R"rawliteral( + + + + + + Upload Successful + + + + +

File uploaded successfully!

+

Returning to main page...

+ + + )rawliteral"; + request->send(200, "text/html", html); + // Restart after short delay to let browser handle redirect + request->onDisconnect([]() { + delay(500); // ensure response is sent + ESP.restart(); + }); + }, + [](AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) { + static File f; + if (index == 0) { + f = LittleFS.open("/config.json", "w"); // start new file + } + if (f) f.write(data, len); // write chunk + if (final) f.close(); // finish file + }); + 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