This page expects the ESP32 web server and API to be on the same origin. The device must enforce the single-controller lock; a page-only lock is not enough.
GET /api/status returns JSON such as {"temperature":28.6,"fanSpeed":62,"mode":"auto","fanOff":false,"occupied":false,"controlGranted":false}.POST /api/connect with {"clientId":"…"} claims control. Return a token on success, or HTTP 423 with {"code":"in_use"} if another user holds the lock.POST /api/status with {"token":"…"} returns status to the controlling client. POST /api/heartbeat and POST /api/disconnect also use the token. Renew the control lease every 6 seconds and expire it after about 20 seconds without a heartbeat.POST /api/control accepts {"token":"…","mode":"manual"}, {"token":"…","mode":"auto"}, {"token":"…","speed":65}, or {"token":"…","off":true}. Each returns JSON such as {"ok":true}.
A GitHub Pages HTTPS page may be blocked from calling a local ESP32 HTTP API. For reliable control, open the copy served by the ESP32 at its local address.