Web: open OTA update page automatically after starting OTA mode

Add a /update endpoint to the HTTPS web panel server that issues a
302 redirect to the OTA update server (http://<host>/update, port 80).
When the user confirms the OTA prompt in the web panel, the browser
now opens the update page in a new tab automatically instead of
requiring manual navigation.  The redirect also helps users who
manually navigate to https://<host>/update and forget to switch to
http — they are transparently forwarded to the correct URL.

The JS handler opens a relative URL (window.location.origin + "/update")
rather than a hardcoded device IP, so it works correctly when the device
is behind a reverse proxy.  Similarly, handleOtaRedirect builds the
redirect target from the request's Host header rather than WiFi.localIP(),
preserving the proxy hostname end-to-end.

The new tab is opened only after runCommand("start ota") returns a
successful response, meaning the OTA server is already up and listening
on port 80 by the time the browser navigates to /update — no race
condition or polling needed.

The /update endpoint returns 404 until notifyOtaStarted() is called,
ensuring the redirect is only served once OTA mode is actually active.

- Add _ota_started flag to WebPanelServer, initialized to false;
  set to true via notifyOtaStarted()
- Register GET /update route; returns 302 to http://<host>/update
  only after OTA has been started (404 otherwise)
- Call notifyOtaStarted() from WebService::prepareForOTAStart() after
  stopping the HTTP redirect server, freeing port 80 for the OTA server
- Update OTA button JS handler to open window.location.origin+"/update"
  in a new tab on successful "start ota" command response
Этот коммит содержится в:
Valentin V. Bartenev
2026-05-09 18:28:48 +03:00
родитель 05ff5fccdb
Коммит bb97a0400e
3 изменённых файлов: 46 добавлений и 2 удалений
+42 -2
Просмотреть файл
@@ -2511,7 +2511,10 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
};
document.getElementById("otaBtn").onclick = async () => {
if (confirm("Start OTA mode now?")) {
await runCommand("start ota");
const result = await runCommand("start ota");
if (result && result.ok) {
window.open(window.location.origin + "/update", "_blank");
}
}
};
const AUTO_REFRESH_INTERVAL = 60;
@@ -2620,7 +2623,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
} // namespace
WebPanelServer::WebPanelServer()
: _runner(nullptr), _server(nullptr), _redirect_server(nullptr), _token{0}, _last_activity_ms(0), _route_context{this} {
: _runner(nullptr), _server(nullptr), _redirect_server(nullptr), _token{0}, _last_activity_ms(0), _route_context{this}, _ota_started(false) {
}
void WebPanelServer::setCommandRunner(WebPanelCommandRunner* runner) {
@@ -2669,12 +2672,14 @@ bool WebPanelServer::start() {
httpd_uri_t login_uri = {.uri = "/login", .method = HTTP_POST, .handler = &WebPanelServer::handleLogin, .user_ctx = &_route_context};
httpd_uri_t command_uri = {.uri = "/api/command", .method = HTTP_POST, .handler = &WebPanelServer::handleCommand, .user_ctx = &_route_context};
httpd_uri_t stats_uri = {.uri = "/api/stats", .method = HTTP_GET, .handler = &WebPanelServer::handleStats, .user_ctx = &_route_context};
httpd_uri_t update = {.uri = "/update", .method = HTTP_GET, .handler = &WebPanelServer::handleOtaRedirect, .user_ctx = &_route_context};
httpd_register_uri_handler(_server, &index_uri);
httpd_register_uri_handler(_server, &app_uri);
httpd_register_uri_handler(_server, &stats_page_uri);
httpd_register_uri_handler(_server, &login_uri);
httpd_register_uri_handler(_server, &command_uri);
httpd_register_uri_handler(_server, &stats_uri);
httpd_register_uri_handler(_server, &update);
httpd_config_t redirect_config = HTTPD_DEFAULT_CONFIG();
redirect_config.server_port = 80;
@@ -2745,6 +2750,10 @@ void WebPanelServer::lockSession() {
_last_activity_ms = 0;
}
void WebPanelServer::notifyOtaStarted() {
_ota_started = true;
}
esp_err_t WebPanelServer::handleIndex(httpd_req_t* req) {
auto* ctx = static_cast<RouteContext*>(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr) {
@@ -2794,6 +2803,34 @@ esp_err_t WebPanelServer::handleStatsPage(httpd_req_t* req) {
return sendProgmem(req, kWebPanelAppHtml);
}
esp_err_t WebPanelServer::handleOtaRedirect(httpd_req_t* req) {
auto* ctx = static_cast<RouteContext*>(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr) {
return httpd_resp_send_500(req);
}
if (!ctx->self->_ota_started) {
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "OTA not started");
return ESP_OK;
}
char host[64] = {};
if (httpd_req_get_hdr_value_str(req, "Host", host, sizeof(host)) == ESP_OK && host[0] != '\0') {
char* colon = strchr(host, ':');
if (colon != nullptr) *colon = '\0';
} else {
strncpy(host, WiFi.localIP().toString().c_str(), sizeof(host) - 1);
host[sizeof(host) - 1] = '\0';
}
char location[80];
snprintf(location, sizeof(location), "http://%s/update", host);
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", location);
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
httpd_resp_set_hdr(req, "Connection", "close");
return httpd_resp_send(req, "", 0);
}
esp_err_t WebPanelServer::handleLogin(httpd_req_t* req) {
auto* ctx = static_cast<RouteContext*>(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) {
@@ -2984,4 +3021,7 @@ bool WebPanelServer::shouldAutoLock(unsigned long) const {
void WebPanelServer::lockSession() {
}
void WebPanelServer::notifyOtaStarted() {
}
#endif
+3
Просмотреть файл
@@ -46,6 +46,7 @@ public:
bool hasSessionToken() const;
bool shouldAutoLock(unsigned long now_ms) const;
void lockSession();
void notifyOtaStarted();
private:
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
@@ -59,6 +60,7 @@ private:
char _token[33];
unsigned long _last_activity_ms;
RouteContext _route_context;
bool _ota_started;
static esp_err_t handleIndex(httpd_req_t* req);
static esp_err_t handleHttpRedirect(httpd_req_t* req);
@@ -67,6 +69,7 @@ private:
static esp_err_t handleLogin(httpd_req_t* req);
static esp_err_t handleCommand(httpd_req_t* req);
static esp_err_t handleStats(httpd_req_t* req);
static esp_err_t handleOtaRedirect(httpd_req_t* req);
bool readRequestBody(httpd_req_t* req, char* buffer, size_t buffer_size) const;
void refreshToken();
+1
Просмотреть файл
@@ -30,6 +30,7 @@ void WebService::prepareForOTAStart() {
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
_suspended_for_ota = true;
_panel.stopRedirectServer();
_panel.notifyOtaStarted();
#endif
}