diff --git a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino index 8f26b97..49f218a 100644 --- a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino +++ b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino @@ -733,11 +733,6 @@ void setupWebServer() { request->send(response); }); - 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.on("/ncsi.txt", HTTP_GET, handleCaptivePortal); // Windows NCSI (variation) - server.on("/cp/success.txt", HTTP_GET, handleCaptivePortal); // Android/Generic Success Check server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(204); // 204 No Content response }); @@ -1477,6 +1472,35 @@ void setupWebServer() { } }); + server.on("/scan", HTTP_GET, [](AsyncWebServerRequest *request) { + int scanStatus = WiFi.scanComplete(); + + // -2 means scan not triggered, -1 means scan in progress + if (scanStatus < -1 || scanStatus == WIFI_SCAN_FAILED) { + // Start the asynchronous scan + WiFi.scanNetworks(true); + request->send(202, "application/json", "{\"status\":\"processing\"}"); + } else if (scanStatus == -1) { + // Scan is currently running + request->send(202, "application/json", "{\"status\":\"processing\"}"); + } else { + // Scan finished (scanStatus >= 0) + String json = "["; + for (int i = 0; i < scanStatus; ++i) { + json += "{"; + json += "\"ssid\":\"" + WiFi.SSID(i) + "\","; + json += "\"rssi\":" + String(WiFi.RSSI(i)); + json += "}"; + if (i < scanStatus - 1) json += ","; + } + json += "]"; + + // Clean up scan results from memory + WiFi.scanDelete(); + request->send(200, "application/json", json); + } + }); + server.on("/ip", HTTP_GET, [](AsyncWebServerRequest *request) { String ip; @@ -1774,27 +1798,38 @@ void setupWebServer() { Serial.println(F("[WEBSERVER] Web server started")); } - void handleCaptivePortal(AsyncWebServerRequest *request) { String uri = request->url(); - // Filter out system-generated probe requests - if (!uri.endsWith("/204") && !uri.endsWith("/ipv6check") && !uri.endsWith("connecttest.txt") && !uri.endsWith("/generate_204") && !uri.endsWith("/fwlink") && !uri.endsWith("/hotspot-detect.html")) { - - Serial.print(F("[WEBSERVER] Captive Portal triggered for URL: ")); - Serial.println(uri); + // Never interfere with real UI or API + if ( + uri == "/" || uri == "/index.html" || uri.startsWith("/config") || uri.startsWith("/hostname") || uri.startsWith("/ip") || uri.endsWith(".json") || uri.endsWith(".js") || uri.endsWith(".css") || uri.endsWith(".png") || uri.endsWith(".ico")) { + return; // let normal handlers serve it } + // Known captive portal probes → redirect + if ( + uri == "/generate_204" || uri == "/gen_204" || uri == "/fwlink" || uri == "/hotspot-detect.html" || uri == "/ncsi.txt" || uri == "/cp/success.txt" || uri == "/library/test/success.html") { + if (isAPMode) { + IPAddress apIP = WiFi.softAPIP(); + String redirectUrl = "http://" + apIP.toString() + "/"; + //Serial.printf("[WEBSERVER] Captive probe %s → redirect\n", uri.c_str()); + request->redirect(redirectUrl); + return; + } + } + + // Unknown URLs in AP mode → redirect (helps odd OSes like /chat) if (isAPMode) { IPAddress apIP = WiFi.softAPIP(); String redirectUrl = "http://" + apIP.toString() + "/"; - Serial.print(F("[WEBSERVER] Redirecting to captive portal: ")); - Serial.println(redirectUrl); + Serial.printf("[WEBSERVER] Captive fallback redirect: %s\n", uri.c_str()); request->redirect(redirectUrl); - } else { - Serial.println(F("[WEBSERVER] Not in AP mode — sending 404")); - request->send(404, "text/plain", "Not found"); + return; } + + // STA mode fallback + request->send(404, "text/plain", "Not found"); } @@ -2401,8 +2436,9 @@ void setup() { } else { Serial.println(F("[SETUP] WiFi state is uncertain after connection attempt.")); } - - setupMDNS(); + if (!isAPMode && WiFi.status() == WL_CONNECTED) { + setupMDNS(); + } setupWebServer(); Serial.println(F("[SETUP] Webserver setup complete")); Serial.println(F("[SETUP] Setup complete")); diff --git a/ESPTimeCast_ESP32/index_html.h b/ESPTimeCast_ESP32/index_html.h index 8d869d9..7d86786 100644 --- a/ESPTimeCast_ESP32/index_html.h +++ b/ESPTimeCast_ESP32/index_html.h @@ -17,6 +17,111 @@ const char index_html[] PROGMEM = R"rawliteral( --accent-color: #0075ff; } + .ssid-wrapper { + position: relative; + } + + .combo-container { + display: flex; + box-sizing: border-box; + width: 100%; + border: 1.5px solid rgba(180, 230, 255, 0.08); + border-radius: 8px; + background-color: rgba(225, 245, 255, 0.07); + color: #ffffff; + font-size: 1rem; + appearance: none; + } + + #ssid { + border-radius: 8px 0 0 8px; + flex: 1; + border: none; + outline: none; + background: transparent; + } + + .icon-btn { + width: 40px; + border: none; + background: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: #666; + transition: background 0.2s; + } + + #arrowBtn { + background: transparent; + border: none; + width: 40px; + border-left: 1.5px solid rgba(180, 230, 255, 0.08); + } + + #arrowBtn > svg{ + position: relative; + top: 2px; + filter: invert(0); + opacity: 1; + } + + #arrowBtn:disabled > svg{ + position: relative; + top: 2px; + opacity: 0.25; + } + + #arrowBtn:hover{ + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(0, 122, 255, 0.35); + } + + #arrowBtn:disabled:hover{ + transform: translateY(0px); + box-shadow: none; + } + + #arrowBtn:disabled{ + cursor: not-allowed; + background: none; + color: rgba(255, 255, 255, 0.250); + } + + #scanBtn { + border-radius: 0 8px 8px 0; + width: 80px; + } + + .icon-btn:hover { background: #f5f5f5; } + #scanBtn:disabled { background: rgba(255, 255, 255, 0.5); cursor: wait; } + + /* The Dropdown Menu */ + #ssidList { + position: absolute; + width: 100%; + max-height: 50vh; + overflow-y: auto; + background: white; + border: 1px solid var(--border-color); + border-radius: 6px; + display: none; + z-index: 1000; + box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); + } + + .ssid-option { + padding: 10px 12px; + cursor: pointer; + color: black; + } + + .ssid-option:hover { + background-color: var(--accent-color); + color: white; + } + * { box-sizing: border-box; } html{ background: radial-gradient(ellipse at 70% 0%, #2b425a 0%, #171e23 100%); @@ -127,8 +232,8 @@ input[type="time"]::-webkit-calendar-picker-indicator, input[type="date"]::-webk input:-webkit-autofill, -input:-webkit-autofill:focus, -input:-webkit-autofill:hover { + input:-webkit-autofill:focus, + input:-webkit-autofill:hover { background: rgba(225,245,255,0.07) !important; color: #fff !important; -webkit-box-shadow: 0 0 0 1000px rgba(225,245,255,0.07) inset !important; @@ -326,8 +431,17 @@ textarea::placeholder {