Wifi connected but can't run the part of the program that needs online status

status is always offline even though wifi is connected to the internet

Cuplikan layar pada 2025-11-20 13-07-06

    server.on("/getcities", HTTP_GET, [](AsyncWebServerRequest *request){
        // Gunakan flag yang dikelola oleh wifiTask — lebih stabil di context async
        Serial.printf("Handler /getcities called. WiFi.status()=%d, wifiConfig.isConnected=%d, FreeHeap=%d\n",
                    WiFi.status(), wifiConfig.isConnected, ESP.getFreeHeap());

        if (!wifiConfig.isConnected || WiFi.status() != WL_CONNECTED) {
            Serial.println("❌ WiFi not connected (early exit)");
            request->send(200, "application/json", "{\"status\":\"offline\"}");
            return;
        }

        Serial.println("📡 Fetching cities from API...");

        // Alokasikan WiFiClientSecure di heap (lebih aman)
        WiFiClientSecure *client = new WiFiClientSecure();
        if (!client) {
            Serial.println("❌ Failed to allocate WiFiClientSecure");
            request->send(200, "application/json", "{\"status\":\"offline\"}");
            return;
        }

        client->setInsecure(); // skip cert verification (OK untuk projek hobi)
        client->setHandshakeTimeout(15000); // beri waktu handshake
        // (optional) client->setBufferSizes(512, 512);

        HTTPClient http;
        http.setTimeout(15000);

        IPAddress ip;
        if (!WiFi.hostByName("api.myquran.com", ip)) {
            Serial.println("❌ DNS lookup failed for api.myquran.com");
            http.end();
            delete client;
            request->send(200, "application/json", "{\"status\":\"offline\"}");
            return;
        }
        Serial.print("Resolved api.myquran.com -> "); Serial.println(ip);

        bool begun = http.begin(*client, "https://api.myquran.com/v2/sholat/kota/semua");
        if (!begun) {
            Serial.println("❌ http.begin() failed");
            http.end();
            delete client;
            request->send(200, "application/json", "{\"status\":\"offline\"}");
            return;
        }

        http.addHeader("Host", "api.myquran.com");

        int httpCode = http.GET();
        Serial.printf("HTTP Response Code: %d\n", httpCode);

        if (httpCode == HTTP_CODE_OK) {
            String payload = http.getString();
            // debug: print preview
            Serial.println("Response preview: " + payload.substring(0, min(200, (int)payload.length())));
            request->send(200, "application/json", payload);
        } else {
            Serial.printf("❌ API Failed with code: %d\n", httpCode);
            if (httpCode > 0) {
                String err = http.getString();
                Serial.println("Error body: " + err);
            }
            request->send(200, "application/json", "{\"status\":\"offline\"}");
        }

        http.end();
        delete client;
    });

Post the full code and description of your circuit

this is the program, the website always displays "connection refused"

/*
 * ESP32-2432S024 + LVGL 9.2.0 + EEZ Studio - Islamic Prayer Clock
 * ARCHITECTURE: FreeRTOS Multi-Task Design - FIXED & WEB API COMPLETED
 * * Update Log:
 * - Added full Web API support for index.html
 * - Added AP Configuration storage
 * - Fixed route mismatches
 */

#include <WiFiClientSecure.h>
#include <lvgl.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>
#include <SPI.h>
#include <LittleFS.h>
#include <FS.h>
#include "ArduinoJson.h"
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include "TimeLib.h"
#include "NTPClient.h"
#include "WiFiUdp.h"
#include "HTTPClient.h"
#include "esp_task_wdt.h"

// EEZ generated files
#include "src/ui.h"
#include "src/screens.h"

// ================================
// PIN DEFINITIONS
// ================================
#define TFT_BL      27
#define TOUCH_CS    33
#define TOUCH_IRQ   36
#define TOUCH_MOSI  13
#define TOUCH_MISO  12
#define TOUCH_CLK   14
#define SCREEN_WIDTH   240
#define SCREEN_HEIGHT  320

// Touch Calibration
#define TS_MIN_X  200
#define TS_MAX_X  3800
#define TS_MIN_Y  200
#define TS_MAX_Y  3800

// ================================
// RTOS CONFIGURATION
// ================================
#define UI_TASK_STACK_SIZE      16384
#define WIFI_TASK_STACK_SIZE    8192
#define NTP_TASK_STACK_SIZE     8192
#define WEB_TASK_STACK_SIZE     16384
#define PRAYER_TASK_STACK_SIZE  8192

#define UI_TASK_PRIORITY        3
#define WIFI_TASK_PRIORITY      2
#define NTP_TASK_PRIORITY       2
#define WEB_TASK_PRIORITY       1
#define PRAYER_TASK_PRIORITY    1


// Task Handles
TaskHandle_t uiTaskHandle = NULL;
TaskHandle_t wifiTaskHandle = NULL;
TaskHandle_t ntpTaskHandle = NULL;
TaskHandle_t webTaskHandle = NULL;
TaskHandle_t prayerTaskHandle = NULL;

// ================================
// SEMAPHORES & MUTEXES
// ================================
SemaphoreHandle_t displayMutex;
SemaphoreHandle_t timeMutex;
SemaphoreHandle_t wifiMutex;
SemaphoreHandle_t settingsMutex;
SemaphoreHandle_t spiMutex;

// Queue for display updates
QueueHandle_t displayQueue;

// ================================
// GLOBAL OBJECTS
// ================================
static lv_display_t *display;
static lv_indev_t *indev;
static uint8_t buf[SCREEN_WIDTH * 10];

TFT_eSPI tft = TFT_eSPI();
SPIClass touchSPI = SPIClass(VSPI);
XPT2046_Touchscreen touch(TOUCH_CS, TOUCH_IRQ);

// ================================
// CONFIGURATION STRUCTURES
// ================================
struct WiFiConfig {
    char apSSID[33];      // Diperbarui untuk menyimpan setting AP
    char apPassword[65];  // Diperbarui untuk menyimpan setting AP
    String routerSSID;
    String routerPassword;
    bool isConnected;
    IPAddress localIP;
};

struct TimeConfig {
    time_t currentTime;
    bool ntpSynced;
    unsigned long lastNTPUpdate;
    String ntpServer;
};

struct PrayerConfig {
    String subuhTime;
    String zuhurTime;
    String asarTime;
    String maghribTime;
    String isyaTime;
    String selectedCityId;
};

struct CityCache {
    String jsonData;
    unsigned long timestamp;
    bool isValid;
} cityCache = {"", 0, false};

const unsigned long CACHE_DURATION = 86400000; // 24 jam dalam milliseconds

// Global configurations
WiFiConfig wifiConfig;
TimeConfig timeConfig;
PrayerConfig prayerConfig;

// ================================
// DISPLAY UPDATE STRUCTURE
// ================================
struct DisplayUpdate {
    enum Type {
        TIME_UPDATE,
        PRAYER_UPDATE,
        STATUS_UPDATE
    } type;
    String data;
};

// ================================
// NETWORK OBJECTS
// ================================
AsyncWebServer server(80);
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

// ================================
// TOUCH VARIABLES
// ================================
bool touchPressed = false;
int16_t lastX = 0;
int16_t lastY = 0;
unsigned long lastTouchTime = 0;

// ================================
// STATE VARIABLES
// ================================
bool colonOn = true;
bool displayNeedsUpdate = false;

enum WiFiState {
    WIFI_IDLE,
    WIFI_CONNECTING,
    WIFI_CONNECTED,
    WIFI_FAILED
};
volatile WiFiState wifiState = WIFI_IDLE;

enum NTPState {
    NTP_IDLE,
    NTP_UPDATING,
    NTP_SYNCED,
    NTP_FAILED
};
volatile NTPState ntpState = NTP_IDLE;

// Forward Declarations
void updateTimeDisplay();
void updatePrayerDisplay();
void getPrayerTimes(String cityId);
void saveWiFiCredentials();
void savePrayerTimes();
void setupServerRoutes(); // New Function Prototype

// ================================
// FLUSH CALLBACK
// ================================
void my_disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
{
    if (spiMutex != NULL && xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
        uint32_t w = area->x2 - area->x1 + 1;
        uint32_t h = area->y2 - area->y1 + 1;
        uint16_t *color_p = (uint16_t *)px_map;

        tft.startWrite();
        tft.setAddrWindow(area->x1, area->y1, w, h);
        tft.pushColors(color_p, w * h);
        tft.endWrite();

        xSemaphoreGive(spiMutex);
    }
    lv_display_flush_ready(disp);
}

// ================================
// TOUCH CALLBACK
// ================================
void my_touchpad_read(lv_indev_t *indev_driver, lv_indev_data_t *data)
{
    static unsigned long lastTouchRead = 0;
    unsigned long now = millis();
    
    if (now - lastTouchRead < 20) {
        data->state = touchPressed ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
        if (touchPressed) {
            data->point.x = lastX;
            data->point.y = lastY;
        }
        return;
    }
    
    lastTouchRead = now;
    
    bool irqActive = (digitalRead(TOUCH_IRQ) == LOW);
    if (irqActive) {
        delayMicroseconds(100);
        if (spiMutex != NULL && xSemaphoreTake(spiMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
            if (touch.touched()) {
                TS_Point p = touch.getPoint();
                if (p.z > 200) {
                    lastX = map(p.x, TS_MIN_X, TS_MAX_X, 0, SCREEN_WIDTH);
                    lastY = map(p.y, TS_MIN_Y, TS_MAX_Y, 0, SCREEN_HEIGHT);
                    lastX = constrain(lastX, 0, SCREEN_WIDTH - 1);
                    lastY = constrain(lastY, 0, SCREEN_HEIGHT - 1);

                    data->state = LV_INDEV_STATE_PR;
                    data->point.x = lastX;
                    data->point.y = lastY;
                    touchPressed = true;
                    xSemaphoreGive(spiMutex);
                    return;
                }
            }
            xSemaphoreGive(spiMutex);
        }
    }
    data->state = LV_INDEV_STATE_REL;
    touchPressed = false;
}

// ================================
// LITTLEFS FUNCTIONS
// ================================
bool init_littlefs() {
    Serial.println("💾 Initializing LittleFS...");
    if (!LittleFS.begin(true)) {
        Serial.println("❌ LittleFS Mount Failed!");
        return false;
    }
    Serial.println("✓ LittleFS Mounted");
    return true;
}

void loadWiFiCredentials() {
    if (xSemaphoreTake(settingsMutex, portMAX_DELAY) == pdTRUE) {
        // Load Router Creds
        if (LittleFS.exists("/wifi_creds.txt")) {
            fs::File file = LittleFS.open("/wifi_creds.txt", "r");
            if (file) {
                wifiConfig.routerSSID = file.readStringUntil('\n');
                wifiConfig.routerPassword = file.readStringUntil('\n');
                wifiConfig.routerSSID.trim();
                wifiConfig.routerPassword.trim();
                file.close();
            }
        }
        // Load AP Creds
        if (LittleFS.exists("/ap_creds.txt")) {
            fs::File file = LittleFS.open("/ap_creds.txt", "r");
            if (file) {
                String ssid = file.readStringUntil('\n');
                String pass = file.readStringUntil('\n');
                ssid.trim();
                pass.trim();
                ssid.toCharArray(wifiConfig.apSSID, 33);
                pass.toCharArray(wifiConfig.apPassword, 65);
                file.close();
            }
        } else {
            // Defaults
            strcpy(wifiConfig.apSSID, "JWS ESP32");
            strcpy(wifiConfig.apPassword, "12345678");
        }
        xSemaphoreGive(settingsMutex);
    }
}

void saveWiFiCredentials() {
    if (xSemaphoreTake(settingsMutex, portMAX_DELAY) == pdTRUE) {
        fs::File file = LittleFS.open("/wifi_creds.txt", "w");
        if (file) {
            file.println(wifiConfig.routerSSID);
            file.println(wifiConfig.routerPassword);
            file.close();
        }
        xSemaphoreGive(settingsMutex);
    }
}

void saveAPCredentials() {
    if (xSemaphoreTake(settingsMutex, portMAX_DELAY) == pdTRUE) {
        fs::File file = LittleFS.open("/ap_creds.txt", "w");
        if (file) {
            file.println(wifiConfig.apSSID);
            file.println(wifiConfig.apPassword);
            file.close();
        }
        xSemaphoreGive(settingsMutex);
    }
}

void loadPrayerTimes() {
    if (xSemaphoreTake(settingsMutex, portMAX_DELAY) == pdTRUE) {
        if (LittleFS.exists("/prayer_times.txt")) {
            fs::File file = LittleFS.open("/prayer_times.txt", "r");
            if (file) {
                prayerConfig.subuhTime = file.readStringUntil('\n'); prayerConfig.subuhTime.trim();
                prayerConfig.zuhurTime = file.readStringUntil('\n'); prayerConfig.zuhurTime.trim();
                prayerConfig.asarTime = file.readStringUntil('\n'); prayerConfig.asarTime.trim();
                prayerConfig.maghribTime = file.readStringUntil('\n'); prayerConfig.maghribTime.trim();
                prayerConfig.isyaTime = file.readStringUntil('\n'); prayerConfig.isyaTime.trim();
                // Load city ID if available
                if (file.available()) {
                    prayerConfig.selectedCityId = file.readStringUntil('\n');
                    prayerConfig.selectedCityId.trim();
                }
                file.close();
            }
        }
        xSemaphoreGive(settingsMutex);
    }
}

void savePrayerTimes() {
    if (xSemaphoreTake(settingsMutex, portMAX_DELAY) == pdTRUE) {
        fs::File file = LittleFS.open("/prayer_times.txt", "w");
        if (file) {
            file.println(prayerConfig.subuhTime);
            file.println(prayerConfig.zuhurTime);
            file.println(prayerConfig.asarTime);
            file.println(prayerConfig.maghribTime);
            file.println(prayerConfig.isyaTime);
            file.println(prayerConfig.selectedCityId); // Save City ID too
            file.close();
        }
        xSemaphoreGive(settingsMutex);
    }
}

// ================================
// TASKS
// ================================

// TASK 1: UI
void uiTask(void *parameter) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xFrequency = pdMS_TO_TICKS(20);
    
    while (true) {
        if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(20)) == pdTRUE) {
            lv_timer_handler();
            xSemaphoreGive(displayMutex);
        }
        
        DisplayUpdate update;
        if (xQueueReceive(displayQueue, &update, 0) == pdTRUE) {
            if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
                switch (update.type) {
                    case DisplayUpdate::TIME_UPDATE: updateTimeDisplay(); break;
                    case DisplayUpdate::PRAYER_UPDATE: updatePrayerDisplay(); break;
                    default: break;
                }
                xSemaphoreGive(displayMutex);
            }
        }
        vTaskDelayUntil(&xLastWakeTime, xFrequency);
    }
}

// TASK 2: WiFi
void wifiTask(void *parameter) {
    while (true) {
        switch (wifiState) {
            case WIFI_IDLE:
                if (wifiConfig.routerSSID.length() > 0 && !wifiConfig.isConnected) {
                    if (xSemaphoreTake(wifiMutex, portMAX_DELAY) == pdTRUE) {
                        WiFi.begin(wifiConfig.routerSSID.c_str(), wifiConfig.routerPassword.c_str());
                        wifiState = WIFI_CONNECTING;
                        xSemaphoreGive(wifiMutex);
                    }
                }
                vTaskDelay(pdMS_TO_TICKS(5000));
                break;
            case WIFI_CONNECTING:
                if (WiFi.status() == WL_CONNECTED) {
                    if (xSemaphoreTake(wifiMutex, portMAX_DELAY) == pdTRUE) {
                        wifiConfig.isConnected = true;  // ← Pastikan ini di-set TRUE
                        wifiConfig.localIP = WiFi.localIP();
                        wifiState = WIFI_CONNECTED;
                        Serial.println("✅ WiFi Connected!");
                        Serial.print("IP: ");
                        Serial.println(wifiConfig.localIP);
                        xSemaphoreGive(wifiMutex);
                        if (ntpTaskHandle != NULL) xTaskNotifyGive(ntpTaskHandle);
                    }
                }
                vTaskDelay(pdMS_TO_TICKS(500));
                break;
            case WIFI_CONNECTED:
                if (WiFi.status() != WL_CONNECTED) {
                    if (xSemaphoreTake(wifiMutex, portMAX_DELAY) == pdTRUE) {
                        wifiConfig.isConnected = false;
                        wifiState = WIFI_IDLE;
                        xSemaphoreGive(wifiMutex);
                    }
                }
                vTaskDelay(pdMS_TO_TICKS(10000));
                break;
            case WIFI_FAILED:
                vTaskDelay(pdMS_TO_TICKS(30000));
                wifiState = WIFI_IDLE;
                break;
        }
        esp_task_wdt_reset();
    }
}

// TASK 3: NTP
void ntpTask(void *parameter) {
    while (true) {
        // 1. TUNGGU DI SINI sampai ada notifikasi (dari tombol Web atau Timer)
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        
        Serial.println("🔄 Melakukan Sync NTP ke Internet...");

        if (xSemaphoreTake(timeMutex, portMAX_DELAY) == pdTRUE) {
            // 2. Update alamat server sesuai settingan terakhir
            timeClient.setPoolServerName(timeConfig.ntpServer.c_str());
            
            // 3. Set Offset ke WIB (Penting agar tidak kembali ke UTC)
            timeClient.setTimeOffset(25200); 

            timeClient.begin();
            
            if (timeClient.update()) {
                timeConfig.currentTime = timeClient.getEpochTime();
                setTime(timeConfig.currentTime);
                timeConfig.ntpSynced = true;
                
                // Kirim ke layar
                DisplayUpdate update;
                update.type = DisplayUpdate::TIME_UPDATE;
                xQueueSend(displayQueue, &update, 0);
                
                Serial.println("✅ NTP Sukses. Waktu terupdate.");
            } else {
                Serial.println("❌ Gagal connect ke NTP Server.");
            }
            xSemaphoreGive(timeMutex);
        }
        
        // Reset watchdog
        esp_task_wdt_reset();
    }
}

// TASK 4: Web Server
void webTask(void *parameter) {
    setupServerRoutes();
    server.begin();
    while (true) {
        vTaskDelay(pdMS_TO_TICKS(1000));
        esp_task_wdt_reset();
    }
}

// TASK 5: Prayer
void prayerTask(void *parameter) {
    while (true) {
        if (xSemaphoreTake(timeMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
            if (hour(timeConfig.currentTime) == 0 && 
                minute(timeConfig.currentTime) == 0 &&
                wifiConfig.isConnected &&
                prayerConfig.selectedCityId.length() > 0) {
                
                xSemaphoreGive(timeMutex);
                getPrayerTimes(prayerConfig.selectedCityId);
                
                DisplayUpdate update;
                update.type = DisplayUpdate::PRAYER_UPDATE;
                xQueueSend(displayQueue, &update, 0);
            } else {
                xSemaphoreGive(timeMutex);
            }
        }
        vTaskDelay(pdMS_TO_TICKS(60000));
        esp_task_wdt_reset();
    }
}

// Clock Tick Task
void clockTickTask(void *parameter) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xFrequency = pdMS_TO_TICKS(1000);
    
    static int autoSyncCounter = 0; 
    
    while (true) {
        if (xSemaphoreTake(timeMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
            timeConfig.currentTime++;
            xSemaphoreGive(timeMutex);
            DisplayUpdate update;
            update.type = DisplayUpdate::TIME_UPDATE;
            xQueueSend(displayQueue, &update, 0);
        }

        // LOGIKA AUTO SYNC TIAP 1 JAM (3600 detik)
        // Auto Sync hanya berjalan jika WiFi terhubung
        if (wifiConfig.isConnected) {
            autoSyncCounter++;
            if (autoSyncCounter >= 3600) { // 3600 detik = 1 jam
                autoSyncCounter = 0;
                if (ntpTaskHandle != NULL) xTaskNotifyGive(ntpTaskHandle); // Panggil NTP Task
            }
        }

        vTaskDelayUntil(&xLastWakeTime, xFrequency);
    }
}

// ================================
// HELPER FUNCTIONS
// ================================
void updateTimeDisplay() {
    if (xSemaphoreTake(timeMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
        char timeStr[20];
        char dateStr[15];
        
        sprintf(timeStr, "%02d%c%02d", 
                hour(timeConfig.currentTime), 
                colonOn ? ':' : ' ', 
                minute(timeConfig.currentTime));
        sprintf(dateStr, "%02d/%02d/%04d", 
                day(timeConfig.currentTime), 
                month(timeConfig.currentTime), 
                year(timeConfig.currentTime));
        
        String dateTimeStr = String(timeStr) + " " + String(dateStr);
        if (objects.date_time) lv_label_set_text(objects.date_time, dateTimeStr.c_str());
        
        colonOn = !colonOn;
        xSemaphoreGive(timeMutex);
    }
}

void updatePrayerDisplay() {
    if(objects.subuh_time) lv_label_set_text(objects.subuh_time, prayerConfig.subuhTime.c_str());
    if(objects.zuhur_time) lv_label_set_text(objects.zuhur_time, prayerConfig.zuhurTime.c_str());
    if(objects.ashar_time) lv_label_set_text(objects.ashar_time, prayerConfig.asarTime.c_str());
    if(objects.maghrib_time) lv_label_set_text(objects.maghrib_time, prayerConfig.maghribTime.c_str());
    if(objects.isya_time) lv_label_set_text(objects.isya_time, prayerConfig.isyaTime.c_str());
}

void getPrayerTimes(String cityId) {
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("❌ WiFi not connected, cannot fetch prayer times");
        return;
    }

    // Format tanggal: YYYY-MM-DD (pakai dash, bukan slash!)
    char dateStr[20];
    sprintf(dateStr, "%04d-%02d-%02d", 
            year(timeConfig.currentTime),
            month(timeConfig.currentTime),
            day(timeConfig.currentTime));
    
    String url = "https://api.myquran.com/v2/sholat/jadwal/" + cityId + "/" + String(dateStr);
    
    Serial.println("🕌 Fetching prayer times from: " + url);
    
    HTTPClient http;
    WiFiClientSecure client;
    client.setInsecure(); // Skip SSL certificate verification
    
    http.begin(client, url);
    http.setTimeout(10000);
    int httpCode = http.GET();

    Serial.printf("HTTP Response Code: %d\n", httpCode);

    if (httpCode == 200) {
        String payload = http.getString();
        Serial.println("Response received, parsing JSON...");
        
        StaticJsonDocument<2048> doc;
        DeserializationError error = deserializeJson(doc, payload);
        
        if (!error) {
            // Cek apakah response valid
            if (doc["status"].as<bool>() && !doc["data"]["jadwal"].isNull()) {
                JsonObject jadwal = doc["data"]["jadwal"];
                
                // Ambil data jadwal shalat
                prayerConfig.subuhTime   = jadwal["subuh"].as<String>();
                prayerConfig.zuhurTime   = jadwal["dzuhur"].as<String>();  // Perhatikan: dzuhur (pakai 'd')
                prayerConfig.asarTime    = jadwal["ashar"].as<String>();
                prayerConfig.maghribTime = jadwal["maghrib"].as<String>();
                prayerConfig.isyaTime    = jadwal["isya"].as<String>();
                
                savePrayerTimes();
                
                Serial.println("✅ Prayer times updated successfully:");
                Serial.println("   Subuh: " + prayerConfig.subuhTime);
                Serial.println("   Dzuhur: " + prayerConfig.zuhurTime);
                Serial.println("   Ashar: " + prayerConfig.asarTime);
                Serial.println("   Maghrib: " + prayerConfig.maghribTime);
                Serial.println("   Isya: " + prayerConfig.isyaTime);
            } else {
                Serial.println("❌ Invalid JSON structure or status false");
            }
        } else {
            Serial.print("❌ JSON parse error: ");
            Serial.println(error.c_str());
        }
    } else {
        Serial.printf("❌ HTTP GET failed, code: %d\n", httpCode);
        if (httpCode > 0) {
            String payload = http.getString();
            Serial.println("Error response: " + payload);
        }
    }
    http.end();
}

bool isWiFiReallyConnected() {
    if (WiFi.status() != WL_CONNECTED) {
        return false;
    }
    
    // Double check dengan ping DNS
    IPAddress testIP;
    if (!WiFi.hostByName("www.google.com", testIP)) {
        return false;
    }
    
    return true;
}

// ================================
// WEB SERVER ROUTES - FULLY FIXED
// ================================
void setupServerRoutes() {
    // Root
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send(LittleFS, "/index.html", "text/html");
    });
    
    // CSS
    server.on("/assets/css/foundation.css", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send(LittleFS, "/assets/css/foundation.css", "text/css");
    });

    // Device Status
    server.on("/devicestatus", HTTP_GET, [](AsyncWebServerRequest *request) {
        char timeStr[20];
        sprintf(timeStr, "%02d:%02d:%02d",
                hour(timeConfig.currentTime),
                minute(timeConfig.currentTime),
                second(timeConfig.currentTime));
        
        // Debug print
        Serial.printf("WiFi.status() = %d\n", WiFi.status());
        Serial.printf("wifiConfig.isConnected = %d\n", wifiConfig.isConnected);
        
        String response = "{";
        response += "\"ssid\":\"" + String(WiFi.SSID()) + "\",";
        response += "\"ip\":\"" + wifiConfig.localIP.toString() + "\",";
        response += "\"ntpSynced\":" + String(timeConfig.ntpSynced ? "true" : "false") + ",";
        response += "\"currentTime\":\"" + String(timeStr) + "\",";
        response += "\"freeHeap\":\"" + String(ESP.getFreeHeap()) + "\"";
        response += "}";
        request->send(200, "application/json", response);
    });

    // Get Cities List from API
    server.on("/getcities", HTTP_GET, [](AsyncWebServerRequest *request){
        Serial.println("\n========================================");
        Serial.println("📡 GET CITIES REQUEST");
        
        // CEK CACHE TERLEBIH DAHULU
        unsigned long now = millis();
        if (cityCache.isValid && 
            cityCache.jsonData.length() > 0 && 
            (now - cityCache.timestamp) < CACHE_DURATION) {
            
            Serial.println("✅ Menggunakan cache (tidak perlu fetch API)");
            Serial.printf("   Cache age: %lu ms\n", now - cityCache.timestamp);
            
            AsyncWebServerResponse *response = request->beginResponse(
                200, 
                "application/json", 
                cityCache.jsonData
            );
            response->addHeader("X-Cache", "HIT");
            request->send(response);
            return;
        }
        
        Serial.println("❌ Cache expired atau kosong, fetch dari API...");
        
        // Validasi WiFi
        if (WiFi.status() != WL_CONNECTED) {
            Serial.println("❌ WiFi not connected");
            request->send(200, "application/json", 
                "{\"status\":\"offline\",\"message\":\"WiFi not connected\"}");
            return;
        }
        
        esp_task_wdt_reset();
        
        // DNS Resolution
        IPAddress apiIP;
        if (!WiFi.hostByName("api.myquran.com", apiIP)) {
            Serial.println("❌ DNS failed");
            request->send(200, "application/json", 
                "{\"status\":\"offline\",\"message\":\"DNS failed\"}");
            return;
        }
        
        Serial.printf("✅ DNS: %s\n", apiIP.toString().c_str());
        
        // HTTP Client
        WiFiClientSecure *client = new WiFiClientSecure();
        if (!client) {
            request->send(200, "application/json", 
                "{\"status\":\"offline\",\"message\":\"Memory error\"}");
            return;
        }
        
        client->setInsecure();
        client->setHandshakeTimeout(10000);
        client->setTimeout(10000);
        
        HTTPClient http;
        http.setTimeout(10000);
        http.setConnectTimeout(8000);
        http.setReuse(false);
        
        String url = "https://api.myquran.com/v2/sholat/kota/semua";
        
        if (!http.begin(*client, url)) {
            delete client;
            request->send(200, "application/json", 
                "{\"status\":\"offline\",\"message\":\"HTTP begin failed\"}");
            return;
        }
        
        http.addHeader("User-Agent", "ESP32-JWS/1.0");
        http.addHeader("Accept", "application/json");
        
        Serial.println("⏳ Fetching from API...");
        esp_task_wdt_reset();
        
        int httpCode = http.GET();
        Serial.printf("📊 HTTP Code: %d\n", httpCode);
        
        if (httpCode == HTTP_CODE_OK) {
            WiFiClient* stream = http.getStreamPtr();
            size_t size = http.getSize();
            
            Serial.printf("📦 Size: %d bytes\n", size);
            
            // Cek memory
            if (ESP.getFreeHeap() < size + 10000) {
                Serial.println("❌ Not enough memory");
                http.end();
                delete client;
                request->send(200, "application/json", 
                    "{\"status\":\"offline\",\"message\":\"Memory low\"}");
                return;
            }
            
            // Baca data
            String payload = "";
            payload.reserve(size + 100);
            
            uint8_t buff[512];
            size_t totalRead = 0;
            
            while (http.connected() && totalRead < size) {
                esp_task_wdt_reset();
                
                size_t available = stream->available();
                if (available) {
                    int c = stream->readBytes(buff, min((size_t)512, available));
                    if (c > 0) {
                        payload += String((char*)buff).substring(0, c);
                        totalRead += c;
                    }
                }
                delay(1);
            }
            
            Serial.printf("✅ Downloaded: %d bytes\n", totalRead);
            
            // Validasi JSON
            StaticJsonDocument<512> testDoc;
            DeserializationError error = deserializeJson(testDoc, payload.substring(0, 512));
            
            if (!error && testDoc["status"].as<bool>()) {
                // SIMPAN KE CACHE
                cityCache.jsonData = payload;
                cityCache.timestamp = millis();
                cityCache.isValid = true;
                
                Serial.println("✅ Data cached successfully");
                
                AsyncWebServerResponse *response = request->beginResponse(
                    200, 
                    "application/json", 
                    payload
                );
                response->addHeader("X-Cache", "MISS");
                request->send(response);
            } else {
                Serial.println("❌ Invalid JSON");
                request->send(200, "application/json", 
                    "{\"status\":\"offline\",\"message\":\"Invalid data\"}");
            }
        } else {
            Serial.printf("❌ HTTP failed: %d\n", httpCode);
            request->send(200, "application/json", 
                "{\"status\":\"offline\",\"message\":\"API error\"}");
        }
        
        http.end();
        delete client;
        esp_task_wdt_reset();
        
        Serial.println("========================================\n");
    });

    server.on("/clearcache", HTTP_GET, [](AsyncWebServerRequest *request){
        cityCache.isValid = false;
        cityCache.jsonData = "";
        cityCache.timestamp = 0;
        Serial.println("🗑️ Cache cleared");
        request->send(200, "text/plain", "Cache cleared");
    });

    // Get Prayer Times
    server.on("/getprayertimes", HTTP_GET, [](AsyncWebServerRequest *request){
        String json = "{";
        json += "\"subuh\":\"" + prayerConfig.subuhTime + "\",";
        json += "\"dzuhur\":\"" + prayerConfig.zuhurTime + "\",";
        json += "\"ashar\":\"" + prayerConfig.asarTime + "\",";
        json += "\"maghrib\":\"" + prayerConfig.maghribTime + "\",";
        json += "\"isya\":\"" + prayerConfig.isyaTime + "\"";
        json += "}";
        request->send(200, "application/json", json);
    });

    // Get Selected City ID
    server.on("/getselectedcity", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send(200, "text/plain", prayerConfig.selectedCityId);
    });

    // Update Prayer Times (Triggered by UI button)
    server.on("/updateprayertimes", HTTP_GET, [](AsyncWebServerRequest *request) {
        if (request->hasParam("cityId")) {
            prayerConfig.selectedCityId = request->getParam("cityId")->value();
            
            Serial.println("🕌 Updating prayer times for city ID: " + prayerConfig.selectedCityId);
            
            // Update immediately
            getPrayerTimes(prayerConfig.selectedCityId);
            
            // Update Display
            DisplayUpdate update;
            update.type = DisplayUpdate::PRAYER_UPDATE;
            xQueueSend(displayQueue, &update, 0);

            // Return new data
            String json = "{";
            json += "\"subuh\":\"" + prayerConfig.subuhTime + "\",";
            json += "\"dzuhur\":\"" + prayerConfig.zuhurTime + "\",";
            json += "\"ashar\":\"" + prayerConfig.asarTime + "\",";
            json += "\"maghrib\":\"" + prayerConfig.maghribTime + "\",";
            json += "\"isya\":\"" + prayerConfig.isyaTime + "\"";
            json += "}";
            
            Serial.println("Sending response: " + json);
            request->send(200, "application/json", json);
        } else {
            request->send(400, "text/plain", "Missing cityId");
        }
    });

    // Set WiFi
    server.on("/setwifi", HTTP_POST, [](AsyncWebServerRequest *request) {
        if (request->hasParam("ssid", true) && request->hasParam("password", true)) {
            wifiConfig.routerSSID = request->getParam("ssid", true)->value();
            wifiConfig.routerPassword = request->getParam("password", true)->value();
            saveWiFiCredentials();
            request->send(200, "text/plain", "WiFi Credentials Saved. Restarting...");
            vTaskDelay(pdMS_TO_TICKS(1000));
            ESP.restart();
        } else {
            request->send(400, "text/plain", "Invalid Data");
        }
    });

    // Set AP
    server.on("/setap", HTTP_POST, [](AsyncWebServerRequest *request) {
        if (request->hasParam("ssid", true) && request->hasParam("password", true)) {
            String ssid = request->getParam("ssid", true)->value();
            String pass = request->getParam("password", true)->value();
            ssid.toCharArray(wifiConfig.apSSID, 33);
            pass.toCharArray(wifiConfig.apPassword, 65);
            saveAPCredentials();
            request->send(200, "text/plain", "AP Settings Saved. Restart required.");
        } else {
            request->send(400, "text/plain", "Invalid Data");
        }
    });

    // Set NTP Server & Trigger Sync
    server.on("/setntpserver", HTTP_POST, [](AsyncWebServerRequest *request) {
        if (request->hasParam("server", true)) {
            timeConfig.ntpServer = request->getParam("server", true)->value();
            
            if (ntpTaskHandle != NULL) {
                xTaskNotifyGive(ntpTaskHandle);
            }

            request->send(200, "text/plain", "Server NTP disimpan. Mengambil waktu dari " + timeConfig.ntpServer + "...");
        } else {
            request->send(400, "text/plain", "Data server tidak ditemukan");
        }
    });

    // Sync Time
    server.on("/synctime", HTTP_POST, [](AsyncWebServerRequest *request) {
        if (request->hasParam("y", true) && request->hasParam("m", true) &&
            request->hasParam("d", true) && request->hasParam("h", true) &&
            request->hasParam("i", true) && request->hasParam("s", true)) {
            
            int y = request->getParam("y", true)->value().toInt();
            int m = request->getParam("m", true)->value().toInt();
            int d = request->getParam("d", true)->value().toInt();
            int h = request->getParam("h", true)->value().toInt();
            int i = request->getParam("i", true)->value().toInt();
            int s = request->getParam("s", true)->value().toInt();
            
            if (xSemaphoreTake(timeMutex, portMAX_DELAY) == pdTRUE) {
                setTime(h, i, s, d, m, y);
                timeConfig.currentTime = now(); 
                timeConfig.ntpSynced = true;
                
                DisplayUpdate update;
                update.type = DisplayUpdate::TIME_UPDATE;
                xQueueSend(displayQueue, &update, 0);
                
                xSemaphoreGive(timeMutex);
            }

            request->send(200, "text/plain", "Waktu berhasil disamakan dengan HP/Laptop!");
            
        } else if (wifiConfig.isConnected && ntpTaskHandle != NULL) {
            xTaskNotifyGive(ntpTaskHandle);
            request->send(200, "text/plain", "Sync NTP Internet...");
        } else {
            request->send(500, "text/plain", "Gagal mengambil data waktu dari browser");
        }
    });

    // Factory Reset
    server.on("/reset", HTTP_POST, [](AsyncWebServerRequest *request) {
        LittleFS.remove("/wifi_creds.txt");
        LittleFS.remove("/prayer_times.txt");
        LittleFS.remove("/ap_creds.txt");
        request->send(200, "text/plain", "Resetting...");
        vTaskDelay(pdMS_TO_TICKS(1000));
        ESP.restart();
    });

    server.onNotFound([](AsyncWebServerRequest *request){
        request->send(404, "text/plain", "Not found");
    });
}

// ================================
// SETUP
// ================================
void setup()
{
    Serial.begin(115200);
    delay(1000);

    // Create mutexes
    displayMutex = xSemaphoreCreateMutex();
    timeMutex = xSemaphoreCreateMutex();
    wifiMutex = xSemaphoreCreateMutex();
    settingsMutex = xSemaphoreCreateMutex();
    spiMutex = xSemaphoreCreateMutex();
    
    displayQueue = xQueueCreate(10, sizeof(DisplayUpdate));
    
    init_littlefs();
    loadWiFiCredentials();
    loadPrayerTimes();
    
    // Init Hardware
    pinMode(TFT_BL, OUTPUT);
    digitalWrite(TFT_BL, HIGH);
    pinMode(TOUCH_IRQ, INPUT_PULLUP);
    
    tft.begin();
    tft.setRotation(1);
    tft.fillScreen(TFT_BLACK);
    
    touchSPI.begin(TOUCH_CLK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS);
    touch.begin(touchSPI);
    touch.setRotation(1);
    
    // Init LVGL
    lv_init();
    lv_tick_set_cb([]() { return (uint32_t)millis(); });
    
    display = lv_display_create(SCREEN_WIDTH, SCREEN_HEIGHT);
    lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565);
    lv_display_set_buffers(display, buf, NULL, sizeof(buf), LV_DISPLAY_RENDER_MODE_PARTIAL);
    lv_display_set_flush_cb(display, my_disp_flush);
    
    indev = lv_indev_create();
    lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
    lv_indev_set_read_cb(indev, my_touchpad_read);
    
    ui_init();
    
    // Init AP
    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(wifiConfig.apSSID, wifiConfig.apPassword);
    Serial.printf("AP Started: %s\n", wifiConfig.apSSID);
    
    // Defaults
    timeConfig.ntpServer = "pool.ntp.org";
    timeConfig.currentTime = 0;
    timeConfig.ntpSynced = false;

    // Start Tasks
    xTaskCreatePinnedToCore(uiTask, "UI", UI_TASK_STACK_SIZE, NULL, UI_TASK_PRIORITY, &uiTaskHandle, 1);
    xTaskCreatePinnedToCore(wifiTask, "WiFi", WIFI_TASK_STACK_SIZE, NULL, WIFI_TASK_PRIORITY, &wifiTaskHandle, 0);
    xTaskCreatePinnedToCore(ntpTask, "NTP", NTP_TASK_STACK_SIZE, NULL, NTP_TASK_PRIORITY, &ntpTaskHandle, 0);
    xTaskCreatePinnedToCore(webTask, "Web", WEB_TASK_STACK_SIZE, NULL, WEB_TASK_PRIORITY, &webTaskHandle, 0);
    xTaskCreatePinnedToCore(prayerTask, "Prayer", PRAYER_TASK_STACK_SIZE, NULL, PRAYER_TASK_PRIORITY, &prayerTaskHandle, 0);
    xTaskCreatePinnedToCore(clockTickTask, "Clock", 4096, NULL, 2, NULL, 0);
}

// ================================
// LOOP
// ================================
void loop()
{
    vTaskDelay(pdMS_TO_TICKS(1000));
    esp_task_wdt_reset();
}

HTML

<div class="large-6 medium-6 small-6 cell">
    <div class="grid-x grid-margin-x">
        <div class="large-12 medium-12 small-12 cell">
            <h4>Jadwal Shalat</h4>
            <div class="grid-x grid-margin-x">
                <!-- Search Box -->
                <div class="large-12 medium-12 small-12 cell">
                    <label>Cari Kota
                        <input type="text" id="citySearch" placeholder="Ketik nama kota..." 
                               style="margin-bottom: 5px;">
                    </label>
                </div>
                
                <!-- Select Dropdown -->
                <div class="large-12 medium-12 small-12 cell">
                    <label>Pilih Kota
                        <select id="citySelect" size="5" style="height: 150px;">
                            <option value="">Loading...</option>
                        </select>
                    </label>
                    <button class="secondary button small" onclick="refreshCities()" 
                            id="refreshBtn" style="margin-top: 5px;">
                        🔄 Refresh List
                    </button>
                    <small id="cityCount" style="display: block; margin-top: 5px; color: #666;">
                        <!-- City count akan muncul di sini -->
                    </small>
                </div>
                
                <div class="large-12 medium-12 small-12 cell">
                    <button class="button" onclick="updatePrayerTimes()">
                        Update Jadwal Shalat
                    </button>
                </div>
            </div>
            <div id="prayerTimes" style="text-align: left; margin-top: 10px;"></div>
        </div>
    </div>
</div>

<script>
// GANTI SEMUA VARIABEL DAN FUNGSI CITIES dengan ini:

let isRefreshing = false;
let allCities = []; // Simpan semua data cities
let filteredCities = []; // Cities yang sudah difilter

function refreshCities() {
    if (isRefreshing) {
        console.log('⚠️ Refresh in progress...');
        return;
    }
    
    isRefreshing = true;
    const refreshBtn = document.getElementById('refreshBtn');
    
    if (refreshBtn) {
        refreshBtn.disabled = true;
        refreshBtn.innerHTML = '⏳ Loading...';
    }
    
    retryCount = 0;
    citiesLoaded = false;
    
    fetchCities().finally(() => {
        setTimeout(() => {
            isRefreshing = false;
            if (refreshBtn) {
                refreshBtn.disabled = false;
                refreshBtn.innerHTML = '🔄 Refresh List';
            }
        }, 1000);
    });
}

function fetchCities() {
    const citySelect = document.getElementById('citySelect');
    
    console.log(`📡 Fetching cities...`);
    
    citySelect.innerHTML = '<option value="">Loading...</option>';
    citySelect.disabled = true;

    return fetch('/getcities', {
        method: 'GET',
        headers: {
            'Cache-Control': 'max-age=86400', // Cache 24 jam
        }
    })
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        // Cek apakah dari cache atau fresh
        const cacheStatus = response.headers.get('X-Cache');
        if (cacheStatus === 'HIT') {
            console.log('✅ Loaded from ESP32 cache');
        } else {
            console.log('📡 Fresh data from API');
        }
        
        return response.json();
    })
    .then(response => {
        console.log('📦 Response received');
        
        citySelect.innerHTML = '';
        
        if (response.status === "offline" || response.status === false) {
            const option = document.createElement('option');
            option.value = "";
            option.textContent = response.message || "Tidak ada koneksi internet";
            citySelect.appendChild(option);
            citySelect.disabled = true;
            console.log('❌ Offline');
            return;
        }

        if (response.status === true && Array.isArray(response.data)) {
            allCities = response.data; // Simpan semua data
            filteredCities = [...allCities]; // Copy untuk filtered
            
            console.log(`✅ ${allCities.length} cities loaded`);
            
            renderCities(filteredCities);
            
            citySelect.disabled = false;
            citiesLoaded = true;
            retryCount = 0;
            
            // Update counter
            updateCityCount(filteredCities.length, allCities.length);
            
            // Restore selected city
            loadSavedCity();
            
            // Enable search
            enableCitySearch();
        } else {
            throw new Error('Invalid data format');
        }
    })
    .catch(error => {
        console.error('❌ Error:', error);
        citySelect.innerHTML = '<option value="">Error loading (klik refresh)</option>';
        citySelect.disabled = true;
        retryCount++;
    });
}

function renderCities(cities) {
    const citySelect = document.getElementById('citySelect');
    citySelect.innerHTML = '';
    
    // Default option
    const defaultOption = document.createElement('option');
    defaultOption.value = "";
    defaultOption.textContent = "-- Pilih Kota --";
    citySelect.appendChild(defaultOption);
    
    // Render hanya 100 pertama untuk performa (user bisa search)
    const citiesToRender = cities.slice(0, 100);
    
    citiesToRender.forEach(city => {
        const option = document.createElement('option');
        option.value = city.id;
        option.textContent = city.lokasi;
        citySelect.appendChild(option);
    });
    
    if (cities.length > 100) {
        const moreOption = document.createElement('option');
        moreOption.value = "";
        moreOption.disabled = true;
        moreOption.textContent = `... dan ${cities.length - 100} lainnya (gunakan search)`;
        citySelect.appendChild(moreOption);
    }
}

function enableCitySearch() {
    const searchInput = document.getElementById('citySearch');
    
    // Remove old listener
    const newSearchInput = searchInput.cloneNode(true);
    searchInput.parentNode.replaceChild(newSearchInput, searchInput);
    
    // Add new listener
    newSearchInput.addEventListener('input', function(e) {
        const searchTerm = e.target.value.toLowerCase().trim();
        
        if (searchTerm === '') {
            filteredCities = [...allCities];
        } else {
            filteredCities = allCities.filter(city => 
                city.lokasi.toLowerCase().includes(searchTerm)
            );
        }
        
        console.log(`🔍 Search: "${searchTerm}" -> ${filteredCities.length} results`);
        
        renderCities(filteredCities);
        updateCityCount(filteredCities.length, allCities.length);
    });
}

function updateCityCount(filtered, total) {
    const countEl = document.getElementById('cityCount');
    if (filtered === total) {
        countEl.textContent = `${total} kota tersedia`;
    } else {
        countEl.textContent = `Menampilkan ${filtered} dari ${total} kota`;
    }
}

// Event listener untuk auto-update saat pilih kota
document.addEventListener('DOMContentLoaded', function() {
    const citySelect = document.getElementById('citySelect');
    if (citySelect) {
        citySelect.addEventListener('change', function() {
            if (this.value) {
                updatePrayerTimes();
            }
        });
    }
});
</script>

Could you add more traces about the WiFi connection and share the serial monitor output?

Also, make sure the files are really accessible. What would this print?

#include <LittleFS.h>

void setup() {
  Serial.begin(115200);
  while (!Serial);

  if (!LittleFS.begin()) {
    Serial.println("FS mount failed");
    return;
  }

  Serial.println("FS mounted");

  if (LittleFS.exists("/index.html")) {
    Serial.println("index.html found");
  } else {
    Serial.println("index.html missing");
  }

  Serial.println("Listing files:");
  File root = LittleFS.open("/");
  File f = root.openNextFile();
  while (f) {
    Serial.println(f.name());
    f = root.openNextFile();
  }
}

void loop() {}

You may want to consider simplifying that sketch to just get the WiFi working.

Be aware that "WiFi connected to the AP" doesn't necessarily mean "connected to the Internet".
Let looks to your screenshot:

Your IP "192.168.*" is inside a "local only" block of IP addresses. You need a properly configured network infrastructure (routers, switches, gateways) so that your device can access the global network.
In addition, the IP address shown in the picture looks strange - 192.168.1E Maybe this is just an erroneous output in Serial, but in general it makes me doubt that your device is connected anywhere.