OK let me try - typed here so to be tested:
the code will assume the setup described by OP
with slight modification to file names : home.htm, scripts.js and styles.css because I like it better like that. 
**AJAX BASED DEMO - CLIENT PULL **
Arduino code
/* ============================================
code is placed under the MIT license
Copyright (c) 2025 J-M-L
For the Arduino Forum : https://forum.arduino.cc/u/j-m-l
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
===============================================
*/
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <SPIFFS.h>
const char* ssid = "xxx";
const char* password = "xxx$xxx$";
AsyncWebServer server(80);
// the variable that is displayed on the web page
long value = 0;
void setup() {
Serial.begin(115200);
// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.write(".");
}
Serial.println("\nConnected to WiFi");
// Mount SPIFFS
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS Mount Failed");
while (true) yield();
return;
}
// Serve home.htm
server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
request->send(SPIFFS, "/home.htm", "text/html");
});
// Serve scripts.js
server.on("/scripts.js", HTTP_GET, [](AsyncWebServerRequest * request) {
request->send(SPIFFS, "/scripts.js", "application/javascript");
});
// Serve style.css
server.on("/styles.css", HTTP_GET, [](AsyncWebServerRequest * request) {
request->send(SPIFFS, "/styles.css", "text/css");
});
// Sending the value
server.on("/request", HTTP_GET, [](AsyncWebServerRequest * request) {
Serial.printf("Got a request for value = %ld\n", value);
char tmpBuf[20]; // enough for a long
snprintf(tmpBuf, sizeof tmpBuf, "%ld", value); // build the text for the value
request->send(200, "text/plain", tmpBuf); // send as plain text (could be a JSON as well if we wanted better content management)
if (++value > 10) value = 0; // cycle value for next request
});
// Start the server
server.begin();
Serial.print("navigate to http://"); Serial.println(WiFi.localIP());
}
void loop() {}
home.htm
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AJAX Demo</title>
<link rel="stylesheet" href="styles.css">
<script src="scripts.js" defer></script>
</head>
<body>
<h1>Current Server Data</h1>
<label for='value'>Value:</label>
<span id='value' style="min-width: 100px; display: inline-block; text-align: center;">?</span>
</body>
</html>
scripts.js
// Function to load the initial value or periodically refresh it
function loadValue() {
var request = new XMLHttpRequest();
request.open('GET', '/request', true);
request.onload = function() {
if (request.status == 200) {
var value = parseFloat(request.responseText); // Convert to a number
// Update the span content
var valueSpan = document.getElementById('value');
valueSpan.innerText = request.responseText;
// Change the background color and text color based on value
if (value <= 3) {
valueSpan.style.backgroundColor = 'blue';
valueSpan.style.color = 'white';
} else if (value <= 7) {
valueSpan.style.backgroundColor = 'green';
valueSpan.style.color = 'black';
} else if (value > 7) {
valueSpan.style.backgroundColor = 'red';
valueSpan.style.color = 'black';
} else {
valueSpan.style.backgroundColor = '';
valueSpan.style.color = 'black';
}
}
};
request.send();
}
// Set interval to refresh data every second
setInterval(loadValue, 1000);
// Call loadValue when the page loads
window.onload = loadValue;
and styles.css
body {
font-family: Arial, sans-serif;
margin: 20px;
padding: 0;
background-color: #f9f9f9;
color: #333;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
label {
font-size: 16px;
margin-right: 10px;
}
#value {
padding: 5px 10px;
border-radius: 4px;
display: inline-block;
min-width: 100px;
text-align: center;
}
Now if you want to do the same thing with websockets and a server push over the web socket
**WEBSOCKET BASED DEMO - SEVER PUSH **
Arduino sketch
/* ============================================
code is placed under the MIT license
Copyright (c) 2025 J-M-L
For the Arduino Forum : https://forum.arduino.cc/u/j-m-l
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
===============================================
*/
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <SPIFFS.h>
#include <AsyncWebSocket.h>
const char* ssid = "xxx";
const char* password = "xx$xxx$";
const unsigned long updateInterval = 1000; // ms
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
unsigned long lastUpdateTime = 0;
long value = 0;
void sendValueOverWebSocketIfNeeded() {
if (ws.count() > 0) { // Only send if there's at least one client connected
char tmpBuf[20];
snprintf(tmpBuf, sizeof(tmpBuf), "%ld", value);
ws.textAll(tmpBuf); // Send the value as plain text (C-string) to all connected clients
Serial.printf("I've sent %s\n", tmpBuf);
if (++value > 10) value = 0; // Cycle value
}
}
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
if (type == WS_EVT_CONNECT) {
Serial.println("New client connected");
} else if (type == WS_EVT_ERROR) {
Serial.println("Error occurred on WebSocket");
}
}
void setup() {
Serial.begin(115200);
// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.write(".");
}
Serial.println("\nConnected to WiFi");
// Mount SPIFFS
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS Mount Failed");
while (true) yield();
return;
}
// Serve home.htm, scripts.js, and styles.css
server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
request->send(SPIFFS, "/home.htm", "text/html");
});
server.on("/scripts.js", HTTP_GET, [](AsyncWebServerRequest * request) {
request->send(SPIFFS, "/scripts.js", "application/javascript");
});
server.on("/styles.css", HTTP_GET, [](AsyncWebServerRequest * request) {
request->send(SPIFFS, "/styles.css", "text/css");
});
// WebSocket event handling
ws.onEvent(onWsEvent);
server.addHandler(&ws);
// Start the server
server.begin();
Serial.print("navigate to http://");
Serial.println(WiFi.localIP());
}
void loop() {
unsigned long currentMillis = millis(); // Get the current time
// Check if 1 second has passed
if (currentMillis - lastUpdateTime >= updateInterval) {
lastUpdateTime = currentMillis;
sendValueOverWebSocketIfNeeded();
}
}
home.htm
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket Demo</title>
<link rel="stylesheet" href="styles.css">
<script src="scripts.js" defer></script>
</head>
<body>
<h1>Current Server Data</h1>
<label for='value'>Value:</label>
<span id='value' style="min-width: 100px; display: inline-block; text-align: center;">?</span>
</body>
</html>
scripts.js
var ws = new WebSocket('ws://' + window.location.hostname + '/ws');
ws.onmessage = function(event) {
var value = parseFloat(event.data); // Parse the number sent from the server
var valueSpan = document.getElementById('value');
valueSpan.innerText = value;
// Change background color based on value
if (value <= 3) {
valueSpan.style.backgroundColor = 'blue';
valueSpan.style.color = 'white';
} else if (value <= 7) {
valueSpan.style.backgroundColor = 'green';
valueSpan.style.color = 'black';
} else if (value > 7) {
valueSpan.style.backgroundColor = 'red';
valueSpan.style.color = 'black';
} else {
valueSpan.style.backgroundColor = '';
valueSpan.style.color = 'black';
}
};
and styles.css
body {
font-family: Arial, sans-serif;
margin: 20px;
padding: 0;
background-color: #f9f9f9;
color: #333;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
label {
font-size: 16px;
margin-right: 10px;
}
#value {
padding: 5px 10px;
border-radius: 4px;
display: inline-block;
min-width: 100px;
text-align: center;
}
What it does:
in both cases you have a simple web page showing a label and a textfield for the value and this value is updated once per second to show the current server value.
In the first example, there I'm using AJAX. The client browser sends a request every second to ask for the updated value and it uses the the DOM to modify the content of the text field.
I added a bit of business logic in the script to make it interesting, if the value is less than 3 then you get a blue background, green under 7 and red above.
At each request the server increments the value and rollover when reaching 10.
In the second example, I'm using a websocket. The client connects and establishes a socket with the server. This websocket is a live link between the ESP32 and the browser and you can discuss over that channel. I'm using it with a simple milis() based update to send over that channel the updated value if there is at least one client on the websocket. (you can open multiple browser window and you'll see them update their value at the same time).
THIS IS TOTALLY UNTESTED - LET ME KNOW IF THAT WORKS
(of course update the SSID and PWD to match your settings)