// =======================================================================
//  УМНЫЙ ВЕНТИЛЯТОР v3.0-TEST  —  ТЕСТОВАЯ ПРОШИВКА ДЛЯ ДЕМОНСТРАЦИИ OTA
//  Функционал ИДЕНТИЧЕН v2.11, интерфейс — полностью новый (Terminal/Dashboard)
//  Загружается по воздуху: браузер http://[IP]/update  ИЛИ  Arduino IDE OTA
// =======================================================================
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266HTTPUpdateServer.h>
#include <ESP8266mDNS.h>
#include <ArduinoOTA.h>
#include <DNSServer.h>
#include <DHT.h>
#include <ArduinoJson.h>
#include <IRrecv.h>
#include <IRremoteESP8266.h>
#include <EEPROM.h>

#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

#define IR_RECEIVE_PIN 5
IRrecv irrecv(IR_RECEIVE_PIN);
decode_results results;

#define CH_BUTTON 0xFF629D
#define RELAY1 12
#define RELAY2 13
#define RELAY3 14
#define RELAY4 15
#define RELAY_ON HIGH
#define RELAY_OFF LOW

ESP8266WebServer server(80);
ESP8266HTTPUpdateServer httpUpdater;
DNSServer dnsServer;
const byte DNS_PORT = 53;

const unsigned long WIFI_TIMEOUT = 15000;
bool apMode = false;
unsigned long wifiStartTime = 0;
String savedSSID = "";
String savedPassword = "";
String currentIP = "";
unsigned long lastConnectionAttempt = 0;
bool connectionInProgress = false;

float temperature = 0;
float humidity = 0;
int fanSpeed = 0;
bool autoMode = false;
bool dhtError = false;
String lastIRCommand = "";
unsigned long lastIRTime = 0;
bool initialized = false;
unsigned long uptimeStart = 0;
int irCommandCount = 0;

float tempThresholds[] = {24.0, 26.0, 28.0};
int lastAutoSpeed = -1;

struct FanTimer {
  bool active = false;
  unsigned long endTime = 0;
  unsigned long duration = 0;
  int targetSpeed = 0;
  unsigned long lastUpdate = 0;
  bool stopRequested = false;
};
FanTimer fanTimer;

struct IRButton {
  uint64_t code;
  String name;
  String function;
};
IRButton buttons[] = {
  {0xFF6897, "0", "Выключить"},
  {0xFF30CF, "1", "Скорость 1"},
  {0xFF18E7, "2", "Скорость 2"},
  {0xFF7A85, "3", "Скорость 3"},
  {0xFF10EF, "4", "Авторежим"},
  {CH_BUTTON, "CH", "Точка доступа"}
};
const int buttonCount = 6;

struct WiFiConfig {
  char ssid[32];
  char password[64];
};
WiFiConfig wifiConfig;

// =====================================================================
// ПРОТОТИПЫ
// =====================================================================
void initializeRelays();
void setFanSpeed(int speed);
int getAutoSpeedForTemperature(float temp);
void updateAutoMode();
void handleIRCommand(uint64_t command);
void loadWiFiConfig();
void saveWiFiConfig();
void startAccessPoint();
void connectToWiFi();
void checkWiFiConnection();
void handleRoot();
void handleGetData();
void handleSetSpeed();
void handleSetThresholds();
void handleSaveWifi();
void handleScanNetworks();
String getUptimeString();
void handleWifiConfig();
void handleConnectWifi();
void handleShowIP();
void handleWifiStatus();
void handleNotFound();
void handleStartAP();
void updateFanTimer();
void startFanTimer(unsigned long seconds, int speed);
void stopFanTimer();
void handleSetTimer();
void handleStopTimer();
String getTimerString();

// =====================================================================
// ГЛАВНАЯ СТРАНИЦА — ТЕРМИНАЛЬНЫЙ / DASHBOARD СТИЛЬ
// =====================================================================
const char root_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>SmartFan OS v3.0</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&display=swap');
  *{margin:0;padding:0;box-sizing:border-box}
  :root{
    --green:#00ff88;--cyan:#00e5ff;--orange:#ff9500;--red:#ff3b5c;
    --purple:#bf5fff;--yellow:#ffe600;--bg:#060a0f;--panel:#0c1420;
    --border:#0e2030;--text:#b0c8d8;--dim:#3a5568;
  }
  body{background:var(--bg);color:var(--text);font-family:'Share Tech Mono',monospace;min-height:100vh;overflow-x:hidden}
  body::before{
    content:'';position:fixed;inset:0;
    background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,255,136,.012) 2px,rgba(0,255,136,.012) 4px);
    pointer-events:none;z-index:0
  }

  /* TOPBAR */
  .topbar{background:rgba(0,229,255,.06);border-bottom:1px solid var(--cyan);padding:10px 20px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;backdrop-filter:blur(10px)}
  .logo{font-family:'Orbitron',monospace;font-size:18px;font-weight:900;color:var(--cyan);letter-spacing:2px;text-shadow:0 0 20px rgba(0,229,255,.5)}
  .logo span{color:var(--green)}
  .ver-badge{background:rgba(0,255,136,.1);border:1px solid var(--green);border-radius:4px;padding:3px 10px;font-size:11px;color:var(--green);letter-spacing:1px}
  .topbar-right{display:flex;align-items:center;gap:12px}
  .conn-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:blink 2s infinite}
  @keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
  .conn-lbl{font-size:12px;color:var(--dim)}

  /* MAIN GRID */
  .grid{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:auto auto auto;gap:12px;padding:16px;max-width:900px;margin:0 auto}
  @media(max-width:700px){.grid{grid-template-columns:1fr}}

  /* PANEL */
  .panel{background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:18px;position:relative;overflow:hidden}
  .panel::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--cyan),transparent)}
  .panel-title{font-family:'Orbitron',monospace;font-size:10px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;margin-bottom:16px;display:flex;align-items:center;gap:8px}
  .panel-title::before{content:'//';color:var(--cyan)}

  /* SENSOR PANEL */
  .sensor-row{display:flex;gap:12px}
  .sensor-block{flex:1;background:rgba(0,0,0,.3);border:1px solid var(--border);border-radius:4px;padding:14px;text-align:center}
  .sensor-val{font-family:'Orbitron',monospace;font-size:42px;font-weight:900;line-height:1;margin-bottom:4px}
  .sensor-val.temp{color:var(--orange);text-shadow:0 0 30px rgba(255,149,0,.4)}
  .sensor-val.hum{color:var(--cyan);text-shadow:0 0 30px rgba(0,229,255,.4)}
  .sensor-unit{font-size:16px;font-weight:400}
  .sensor-lbl{font-size:11px;color:var(--dim);letter-spacing:1px}

  /* SPEED PANEL */
  .speed-display{text-align:center;margin-bottom:16px}
  .speed-big{font-family:'Orbitron',monospace;font-size:72px;font-weight:900;line-height:1;color:var(--green);text-shadow:0 0 40px rgba(0,255,136,.5)}
  .speed-lbl{font-size:13px;color:var(--dim);margin-top:4px;letter-spacing:1px}
  .spd-btns{display:grid;grid-template-columns:repeat(5,1fr);gap:8px}
  .spd-btn{background:rgba(0,0,0,.4);border:1px solid var(--border);border-radius:4px;padding:12px 6px;text-align:center;cursor:pointer;transition:all .15s;font-family:'Orbitron',monospace}
  .spd-btn:hover{border-color:var(--cyan);background:rgba(0,229,255,.05)}
  .spd-btn.active{border-color:var(--green);background:rgba(0,255,136,.08);box-shadow:0 0 12px rgba(0,255,136,.2)}
  .spd-btn .num{font-size:22px;font-weight:700;color:var(--text)}
  .spd-btn.active .num{color:var(--green);text-shadow:0 0 10px var(--green)}
  .spd-btn .tag{font-size:9px;color:var(--dim);letter-spacing:.5px;margin-top:4px}
  .s0 .num{color:var(--dim)}
  .s1 .num{color:var(--green)}
  .s2 .num{color:var(--yellow)}
  .s3 .num{color:var(--red)}
  .s4 .num{color:var(--purple)}

  /* STATUS PANEL */
  .stat-row{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.04);font-size:13px}
  .stat-row:last-child{border-bottom:none}
  .stat-key{color:var(--dim)}
  .stat-val{color:var(--text);text-align:right}
  .stat-val.ok{color:var(--green)}
  .stat-val.warn{color:var(--orange)}
  .stat-val.err{color:var(--red)}

  /* CONTROLS */
  .ctrl-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}
  .ctrl-btn{background:rgba(0,0,0,.4);border:1px solid var(--border);border-radius:4px;padding:14px;cursor:pointer;display:flex;align-items:center;gap:12px;transition:all .15s;color:var(--text);font-family:'Share Tech Mono',monospace;font-size:14px;width:100%;text-align:left}
  .ctrl-btn:hover{border-color:var(--cyan);background:rgba(0,229,255,.05)}
  .ctrl-btn .ico{width:34px;height:34px;border-radius:4px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
  .ctrl-btn .ico svg{width:16px;height:16px;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
  .ico-timer{background:rgba(191,95,255,.1);border:1px solid rgba(191,95,255,.2)}.ico-timer svg{stroke:var(--purple)}
  .ico-auto{background:rgba(0,229,255,.1);border:1px solid rgba(0,229,255,.2)}.ico-auto svg{stroke:var(--cyan)}
  .ico-ota{background:rgba(0,255,136,.1);border:1px solid rgba(0,255,136,.2)}.ico-ota svg{stroke:var(--green)}
  .ico-wifi{background:rgba(255,149,0,.1);border:1px solid rgba(255,149,0,.2)}.ico-wifi svg{stroke:var(--orange)}
  .ctrl-texts{display:flex;flex-direction:column;gap:2px}
  .ctrl-title{font-size:13px;color:var(--text)}
  .ctrl-desc{font-size:11px;color:var(--dim)}

  /* TIMER BAR */
  .tmr-bar{display:none;background:rgba(191,95,255,.06);border:1px solid rgba(191,95,255,.3);border-radius:4px;padding:12px 16px;margin-bottom:10px;animation:glow-pulse 2s infinite}
  @keyframes glow-pulse{0%,100%{border-color:rgba(191,95,255,.3)}50%{border-color:rgba(191,95,255,.7);box-shadow:0 0 20px rgba(191,95,255,.15)}}
  .tmr-bar .tmr-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
  .tmr-label{font-family:'Orbitron',monospace;font-size:10px;color:var(--purple);letter-spacing:2px}
  .tmr-clock{font-family:'Orbitron',monospace;font-size:28px;font-weight:700;color:var(--purple);text-shadow:0 0 20px rgba(191,95,255,.5);letter-spacing:3px}
  .tmr-stop-btn{background:rgba(255,59,92,.1);border:1px solid rgba(255,59,92,.3);border-radius:3px;padding:6px 12px;color:var(--red);font-size:12px;cursor:pointer;font-family:'Share Tech Mono',monospace;transition:all .15s}
  .tmr-stop-btn:hover{background:rgba(255,59,92,.2)}

  /* MODALS */
  .modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.85);display:none;align-items:center;justify-content:center;z-index:500;padding:16px;backdrop-filter:blur(6px)}
  .modal-bg.show{display:flex}
  .modal{background:var(--panel);border:1px solid var(--cyan);border-radius:6px;padding:24px;max-width:480px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 0 40px rgba(0,229,255,.15)}
  .modal-title{font-family:'Orbitron',monospace;font-size:14px;color:var(--cyan);letter-spacing:2px;margin-bottom:20px;display:flex;justify-content:space-between;align-items:center}
  .modal-close{background:none;border:none;color:var(--dim);font-size:20px;cursor:pointer;font-family:monospace}
  .modal-close:hover{color:var(--red)}
  .field-lbl{font-size:11px;color:var(--dim);letter-spacing:1px;margin-bottom:6px}
  .field-input{width:100%;padding:12px 14px;background:rgba(0,0,0,.5);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:'Share Tech Mono',monospace;font-size:16px;outline:none;transition:border .15s}
  .field-input:focus{border-color:var(--cyan)}
  .field-input::-webkit-outer-spin-button,.field-input::-webkit-inner-spin-button{-webkit-appearance:none}
  .mgrp{margin-bottom:16px}
  .msel{width:100%;padding:12px 14px;background:rgba(0,0,0,.5);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:'Share Tech Mono',monospace;font-size:14px;outline:none;-webkit-appearance:none}
  .mact{width:100%;padding:14px;border:none;border-radius:4px;font-family:'Orbitron',monospace;font-size:13px;letter-spacing:1px;cursor:pointer;margin-top:8px;display:flex;align-items:center;justify-content:center;gap:10px;transition:all .15s}
  .mact-green{background:rgba(0,255,136,.15);border:1px solid var(--green);color:var(--green)}
  .mact-green:hover{background:rgba(0,255,136,.25)}
  .mact-green:disabled{opacity:.4;cursor:not-allowed}
  .mact-cyan{background:rgba(0,229,255,.15);border:1px solid var(--cyan);color:var(--cyan)}
  .mact-cyan:hover{background:rgba(0,229,255,.25)}
  .mact-purple{background:rgba(191,95,255,.15);border:1px solid var(--purple);color:var(--purple)}
  .mact-purple:hover{background:rgba(191,95,255,.25)}
  .thr-row{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:16px}
  .thr-card{background:rgba(0,0,0,.3);border:1px solid var(--border);border-radius:4px;padding:12px;text-align:center}
  .thr-card .thr-lbl{font-size:10px;color:var(--dim);letter-spacing:1px;margin-bottom:8px}
  .thr-in{width:100%;padding:10px;background:rgba(0,0,0,.5);border:1px solid var(--border);border-radius:3px;color:var(--cyan);font-family:'Orbitron',monospace;font-size:22px;font-weight:700;text-align:center;outline:none;-webkit-appearance:none;-moz-appearance:textfield}
  .thr-in:focus{border-color:var(--cyan)}

  /* OTA */
  .ota-zone{border:1px dashed rgba(0,255,136,.3);border-radius:4px;padding:28px;text-align:center;cursor:pointer;margin-bottom:14px;background:rgba(0,255,136,.02);transition:all .2s}
  .ota-zone:hover,.ota-zone.drag{border-color:var(--green);background:rgba(0,255,136,.06)}
  .ota-zone-ico{font-family:'Orbitron',monospace;font-size:32px;color:rgba(0,255,136,.3);margin-bottom:10px}
  .ota-fname{margin-top:10px;font-size:13px;color:var(--green);display:none}
  .ota-progress{display:none;margin-top:14px}
  .ota-prog-track{width:100%;height:6px;background:rgba(0,0,0,.5);border-radius:2px;overflow:hidden;border:1px solid var(--border)}
  .ota-prog-fill{height:100%;background:linear-gradient(90deg,var(--green),var(--cyan));width:0%;transition:width .3s}
  .ota-prog-txt{font-size:12px;color:var(--dim);margin-top:8px;text-align:center}
  .ota-warn{background:rgba(255,149,0,.06);border:1px solid rgba(255,149,0,.25);border-radius:4px;padding:12px;font-size:12px;color:var(--orange);margin-bottom:14px;line-height:1.5}

  /* WIFI BAR */
  .wbar{background:rgba(255,149,0,.04);border:1px solid rgba(255,149,0,.15);border-radius:4px;padding:10px 16px;display:flex;align-items:center;gap:14px;margin-bottom:12px}
  .wbar-ico svg{width:18px;height:18px;fill:none;stroke:var(--orange);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
  .wbar-ssid{font-size:13px;color:var(--text)}
  .wbar-ip{font-family:'Orbitron',monospace;font-size:11px;color:var(--cyan);margin-left:auto}
  .wbar-btn{background:none;border:1px solid rgba(255,149,0,.25);border-radius:3px;padding:5px 10px;color:var(--orange);font-family:'Share Tech Mono',monospace;font-size:11px;cursor:pointer;text-decoration:none;transition:all .15s}
  .wbar-btn:hover{background:rgba(255,149,0,.1)}

  /* NOTIF */
  .notif{position:fixed;top:16px;right:16px;padding:12px 20px;border-radius:4px;z-index:1000;opacity:0;transform:translateX(110%);transition:all .3s;font-size:14px;font-family:'Share Tech Mono',monospace;border:1px solid var(--green);background:rgba(0,255,136,.1);color:var(--green);max-width:340px}
  .notif.show{opacity:1;transform:none}
  .notif.err{border-color:var(--red);background:rgba(255,59,92,.1);color:var(--red)}
  .notif.info{border-color:var(--cyan);background:rgba(0,229,255,.1);color:var(--cyan)}
  .spin{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.2);border-radius:50%;border-top-color:currentColor;animation:sp 1s linear infinite}
  @keyframes sp{to{transform:rotate(360deg)}}

  /* SCAN */
  .scan-list{max-height:200px;overflow-y:auto;margin-bottom:12px}
  .scan-item{padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.04);cursor:pointer;font-size:13px;display:flex;justify-content:space-between;transition:background .15s}
  .scan-item:hover{background:rgba(0,229,255,.05)}
  .scan-item.selected{background:rgba(0,229,255,.08);color:var(--cyan)}
  .scan-rssi{font-size:11px;color:var(--dim)}
  .no-nets{text-align:center;color:var(--dim);padding:20px;font-size:12px}

  /* SPAN FULL */
  .span-full{grid-column:1/-1}

  .prompt{color:var(--green);margin-right:6px}
</style>
</head>
<body>

<!-- TOPBAR -->
<div class="topbar">
  <div class="logo">SMART<span>FAN</span></div>
  <div class="topbar-right">
    <div class="conn-dot"></div>
    <span class="conn-lbl" id="conn-lbl">—.—.—.—</span>
    <div class="ver-badge">v3.0-TEST</div>
  </div>
</div>

<!-- WIFI BAR -->
<div style="padding:12px 16px 0;max-width:900px;margin:0 auto">
  <div class="wbar">
    <div class="wbar-ico"><svg viewBox="0 0 24 24"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></div>
    <span class="wbar-ssid" id="wbar-ssid">Подключение...</span>
    <span class="wbar-ip" id="wbar-ip">—</span>
    <a href="/wifi_config" class="wbar-btn">⚙ Wi-Fi</a>
  </div>
</div>

<!-- GRID -->
<div class="grid">

  <!-- SENSOR -->
  <div class="panel">
    <div class="panel-title">СЕНСОРЫ DHT22</div>
    <div class="sensor-row">
      <div class="sensor-block">
        <div class="sensor-val temp" id="temp-val">—<span class="sensor-unit">°C</span></div>
        <div class="sensor-lbl">ТЕМПЕРАТУРА</div>
      </div>
      <div class="sensor-block">
        <div class="sensor-val hum" id="hum-val">—<span class="sensor-unit">%</span></div>
        <div class="sensor-lbl">ВЛАЖНОСТЬ</div>
      </div>
    </div>
  </div>

  <!-- SPEED -->
  <div class="panel">
    <div class="panel-title">УПРАВЛЕНИЕ СКОРОСТЬЮ</div>
    <div class="speed-display">
      <div class="speed-big" id="spd-big">0</div>
      <div class="speed-lbl" id="spd-lbl">ВЕНТИЛЯТОР ВЫКЛЮЧЕН</div>
    </div>
    <div class="spd-btns">
      <div class="spd-btn s0" id="b0" onclick="setSpeed(0)"><div class="num">0</div><div class="tag">ВЫКЛ</div></div>
      <div class="spd-btn s1" id="b1" onclick="setSpeed(1)"><div class="num">1</div><div class="tag">ТИХО</div></div>
      <div class="spd-btn s2" id="b2" onclick="setSpeed(2)"><div class="num">2</div><div class="tag">НОРМ</div></div>
      <div class="spd-btn s3" id="b3" onclick="setSpeed(3)"><div class="num">3</div><div class="tag">МАКС</div></div>
      <div class="spd-btn s4" id="b4" onclick="setSpeed(4)"><div class="num">A</div><div class="tag">АВТО</div></div>
    </div>
  </div>

  <!-- STATUS -->
  <div class="panel">
    <div class="panel-title">СИСТЕМНЫЙ СТАТУС</div>
    <div class="stat-row"><span class="stat-key"><span class="prompt">&gt;</span> АПТАЙМ</span><span class="stat-val" id="s-uptime">—</span></div>
    <div class="stat-row"><span class="stat-key"><span class="prompt">&gt;</span> СВОБОДНАЯ ПАМЯТЬ</span><span class="stat-val ok" id="s-heap">—</span></div>
    <div class="stat-row"><span class="stat-key"><span class="prompt">&gt;</span> ИК КОМАНДЫ</span><span class="stat-val" id="s-ir">0</span></div>
    <div class="stat-row"><span class="stat-key"><span class="prompt">&gt;</span> ПОСЛЕДНИЙ ИК</span><span class="stat-val" id="s-lastir">—</span></div>
    <div class="stat-row"><span class="stat-key"><span class="prompt">&gt;</span> ПОРОГИ АВТО</span><span class="stat-val" id="s-thr">—</span></div>
    <div class="stat-row"><span class="stat-key"><span class="prompt">&gt;</span> ДАТЧИК DHT22</span><span class="stat-val ok" id="s-dht">ОК</span></div>
  </div>

  <!-- CONTROLS -->
  <div class="panel">
    <div class="panel-title">ДЕЙСТВИЯ</div>
    <div class="tmr-bar" id="tmr-bar">
      <div class="tmr-top">
        <span class="tmr-label">// ТАЙМЕР АКТИВЕН</span>
        <div class="tmr-clock" id="tmr-clock">00:00:00</div>
        <button class="tmr-stop-btn" onclick="stopTimer()">СТОП</button>
      </div>
    </div>
    <div class="ctrl-grid">
      <button class="ctrl-btn" onclick="openTimerModal()">
        <div class="ico ico-timer"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><polyline points="12 6 12 12 16 14"/></svg></div>
        <div class="ctrl-texts"><div class="ctrl-title">ТАЙМЕР</div><div class="ctrl-desc">Авто-выключение</div></div>
      </button>
      <button class="ctrl-btn" onclick="openAutoModal()">
        <div class="ico ico-auto"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/></svg></div>
        <div class="ctrl-texts"><div class="ctrl-title">АВТОРЕЖИМ</div><div class="ctrl-desc">Пороги темп.</div></div>
      </button>
      <button class="ctrl-btn" onclick="openOtaModal()">
        <div class="ico ico-ota"><svg viewBox="0 0 24 24"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/></svg></div>
        <div class="ctrl-texts"><div class="ctrl-title">OTA UPDATE</div><div class="ctrl-desc">Прошивка .bin</div></div>
      </button>
      <a href="/wifi_config" style="text-decoration:none">
        <button class="ctrl-btn" style="width:100%">
          <div class="ico ico-wifi"><svg viewBox="0 0 24 24"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg></div>
          <div class="ctrl-texts"><div class="ctrl-title">WI-FI</div><div class="ctrl-desc">Настройка сети</div></div>
        </button>
      </a>
    </div>
  </div>

</div><!-- /grid -->

<!-- MODAL: TIMER -->
<div class="modal-bg" id="modal-tmr">
  <div class="modal">
    <div class="modal-title">// УСТАНОВИТЬ ТАЙМЕР <button class="modal-close" onclick="closeModal('modal-tmr')">×</button></div>
    <div class="mgrp">
      <div class="thr-row">
        <div class="thr-card"><div class="thr-lbl">ЧАСЫ</div><input type="number" class="thr-in" id="t-h" value="0" min="0" max="23"></div>
        <div class="thr-card"><div class="thr-lbl">МИНУТЫ</div><input type="number" class="thr-in" id="t-m" value="30" min="0" max="59"></div>
        <div class="thr-card"><div class="thr-lbl">СЕКУНДЫ</div><input type="number" class="thr-in" id="t-s" value="0" min="0" max="59"></div>
      </div>
    </div>
    <div class="mgrp"><div class="field-lbl">СКОРОСТЬ ВО ВРЕМЯ ТАЙМЕРА</div>
      <select class="msel" id="t-spd">
        <option value="1">1 — Низкая</option>
        <option value="2" selected>2 — Средняя</option>
        <option value="3">3 — Высокая</option>
        <option value="4">A — Авторежим</option>
      </select>
    </div>
    <button class="mact mact-purple" onclick="startTimer()">▶ ЗАПУСТИТЬ ТАЙМЕР</button>
  </div>
</div>

<!-- MODAL: AUTO -->
<div class="modal-bg" id="modal-auto">
  <div class="modal">
    <div class="modal-title">// ПОРОГИ АВТОРЕЖИМА <button class="modal-close" onclick="closeModal('modal-auto')">×</button></div>
    <div class="thr-row">
      <div class="thr-card"><div class="thr-lbl">ПОРОГ 1</div><input type="number" class="thr-in" id="t1" step="0.5" min="15" max="35"><div style="font-size:10px;color:var(--dim);margin-top:6px">Выключен</div></div>
      <div class="thr-card"><div class="thr-lbl">ПОРОГ 2</div><input type="number" class="thr-in" id="t2" step="0.5" min="15" max="35"><div style="font-size:10px;color:var(--dim);margin-top:6px">Скорость 1</div></div>
      <div class="thr-card"><div class="thr-lbl">ПОРОГ 3</div><input type="number" class="thr-in" id="t3" step="0.5" min="15" max="35"><div style="font-size:10px;color:var(--dim);margin-top:6px">Скорость 2</div></div>
    </div>
    <button class="mact mact-cyan" onclick="saveThresholds()">✓ СОХРАНИТЬ</button>
  </div>
</div>

<!-- MODAL: OTA -->
<div class="modal-bg" id="modal-ota">
  <div class="modal">
    <div class="modal-title">// OTA FIRMWARE UPDATE <button class="modal-close" onclick="closeModal('modal-ota')">×</button></div>
    <div class="ota-warn">⚠ Вентилятор выключится на время прошивки. После загрузки устройство перезагрузится (~15 сек).</div>
    <div class="ota-zone" id="ota-drop" onclick="document.getElementById('ota-file').click()">
      <div class="ota-zone-ico">↑</div>
      <div style="font-size:13px;color:var(--dim)">Нажмите или перетащите .bin файл</div>
      <div class="ota-fname" id="ota-file-name"></div>
    </div>
    <input type="file" id="ota-file" accept=".bin" style="display:none" onchange="onFileSelected(this)">
    <div class="ota-progress" id="ota-progress">
      <div class="ota-prog-track"><div class="ota-prog-fill" id="ota-fill"></div></div>
      <div class="ota-prog-txt" id="ota-prog-txt">Загрузка...</div>
    </div>
    <button class="mact mact-green" id="ota-btn" onclick="startOTA()" disabled>↑ ЗАГРУЗИТЬ ПРОШИВКУ</button>
  </div>
</div>

<div id="notif" class="notif"></div>

<script>
var curSpd=0,otaFile=null;
var tmrActive=false,tmrLeft=0,tmrInterval=null;

var spdLabels=['ВЕНТИЛЯТОР ВЫКЛЮЧЕН','СКОРОСТЬ 1 — ТИХО','СКОРОСТЬ 2 — НОРМАЛЬНАЯ','СКОРОСТЬ 3 — МАКСИМУМ','АВТОРЕЖИМ ВКЛЮЧЁН'];
var spdColors=['var(--dim)','var(--green)','var(--yellow)','var(--red)','var(--purple)'];

function notify(msg,type){
  var el=document.getElementById('notif');
  el.textContent=msg;el.className='notif '+(type||'');el.classList.add('show');
  clearTimeout(el._t);el._t=setTimeout(function(){el.classList.remove('show');},4000);
}

function setSpeed(s){
  fetch('/setSpeed?speed='+s).then(function(r){return r.json();}).then(function(d){
    if(d.success)notify(d.message,'');else notify('Ошибка','err');
  }).catch(function(){notify('Ошибка сети','err');});
}

function updateUI(d){
  // temp/hum
  document.getElementById('temp-val').innerHTML=d.dhtError?'ERR<span class="sensor-unit">°C</span>':(d.temp.toFixed(1)+'<span class="sensor-unit">°C</span>');
  document.getElementById('hum-val').innerHTML=d.dhtError?'ERR<span class="sensor-unit">%</span>':(d.hum.toFixed(1)+'<span class="sensor-unit">%</span>');
  // speed
  var spd=d.autoMode?4:d.speed;curSpd=spd;
  document.getElementById('spd-big').textContent=spd===4?'A':spd;
  document.getElementById('spd-big').style.color=spdColors[spd];
  document.getElementById('spd-big').style.textShadow='0 0 40px '+spdColors[spd].replace('var(','').replace(')','');
  document.getElementById('spd-lbl').textContent=spdLabels[spd];
  for(var i=0;i<=4;i++){var b=document.getElementById('b'+i);if(b)b.classList.toggle('active',i===spd);}
  // status
  document.getElementById('s-uptime').textContent=d.uptime;
  document.getElementById('s-heap').textContent=d.freeHeap+' B';
  document.getElementById('s-ir').textContent=d.irCount;
  document.getElementById('s-lastir').textContent=d.lastIR||'—';
  if(d.thresholds)document.getElementById('s-thr').textContent=d.thresholds[0]+'° / '+d.thresholds[1]+'° / '+d.thresholds[2]+'°';
  document.getElementById('s-dht').textContent=d.dhtError?'ОШИБКА':'ОК';
  document.getElementById('s-dht').className='stat-val '+(d.dhtError?'err':'ok');
  // timer
  if(d.timer&&d.timer.active){
    tmrActive=true;tmrLeft=d.timer.secondsLeft;
    document.getElementById('tmr-bar').style.display='block';
    updateTimerClock();
    if(!tmrInterval)tmrInterval=setInterval(function(){if(tmrLeft>0){tmrLeft--;updateTimerClock();}},1000);
  } else {
    tmrActive=false;document.getElementById('tmr-bar').style.display='none';
    if(tmrInterval){clearInterval(tmrInterval);tmrInterval=null;}
  }
}

function updateTimerClock(){
  var s=tmrLeft%60,m=Math.floor(tmrLeft/60)%60,h=Math.floor(tmrLeft/3600);
  document.getElementById('tmr-clock').textContent=(h<10?'0':'')+h+':'+(m<10?'0':'')+m+':'+(s<10?'0':'')+s;
}

function fetchData(){
  fetch('/getData').then(function(r){return r.json();}).then(function(d){updateUI(d);}).catch(function(){});
}

function fetchStatus(){
  fetch('/wifi_status').then(function(r){return r.json();}).then(function(d){
    document.getElementById('conn-lbl').textContent=d.ip;
    document.getElementById('wbar-ssid').textContent=d.mode==='ap'?'AP: SmartFan_Config':'WiFi: '+d.ssid;
    document.getElementById('wbar-ip').textContent=d.ip;
  }).catch(function(){});
}

// THRESHOLDS
function openAutoModal(){
  fetch('/getData').then(function(r){return r.json();}).then(function(d){
    if(d.thresholds){document.getElementById('t1').value=d.thresholds[0];document.getElementById('t2').value=d.thresholds[1];document.getElementById('t3').value=d.thresholds[2];}
    document.getElementById('modal-auto').classList.add('show');
  });
}
function saveThresholds(){
  var a=parseFloat(document.getElementById('t1').value);
  var b=parseFloat(document.getElementById('t2').value);
  var c=parseFloat(document.getElementById('t3').value);
  if(a>=b||b>=c){notify('Порядок порогов нарушен','err');return;}
  fetch('/setThresholds?t1='+a+'&t2='+b+'&t3='+c).then(function(){
    notify('Пороги сохранены','');closeModal('modal-auto');fetchData();
  });
}

// TIMER
function openTimerModal(){document.getElementById('modal-tmr').classList.add('show');}
function startTimer(){
  var h=parseInt(document.getElementById('t-h').value)||0;
  var m=parseInt(document.getElementById('t-m').value)||0;
  var s=parseInt(document.getElementById('t-s').value)||0;
  var spd=parseInt(document.getElementById('t-spd').value);
  fetch('/setTimer?hours='+h+'&minutes='+m+'&seconds='+s+'&speed='+spd)
    .then(function(r){return r.json();}).then(function(d){
      if(d.success){notify(d.message,'');closeModal('modal-tmr');}else notify(d.message,'err');
    });
}
function stopTimer(){
  fetch('/stopTimer').then(function(r){return r.json();}).then(function(d){
    if(d.success){notify('Таймер остановлен','');fetchData();}
  });
}

// OTA
function openOtaModal(){otaFile=null;document.getElementById('ota-btn').disabled=true;document.getElementById('ota-file-name').style.display='none';document.getElementById('ota-progress').style.display='none';document.getElementById('modal-ota').classList.add('show');}
function onFileSelected(inp){
  if(inp.files&&inp.files[0]){
    otaFile=inp.files[0];
    var fn=document.getElementById('ota-file-name');fn.textContent='▸ '+otaFile.name;fn.style.display='block';
    document.getElementById('ota-btn').disabled=false;
  }
}
function startOTA(){
  if(!otaFile){notify('Выберите .bin файл','err');return;}
  var form=new FormData();form.append('image',otaFile);
  document.getElementById('ota-btn').disabled=true;
  document.getElementById('ota-progress').style.display='block';
  var xhr=new XMLHttpRequest();
  xhr.open('POST','/update');
  xhr.upload.onprogress=function(e){
    if(e.lengthComputable){
      var pct=Math.round(e.loaded*100/e.total);
      document.getElementById('ota-fill').style.width=pct+'%';
      document.getElementById('ota-prog-txt').textContent='Загрузка: '+pct+'%';
    }
  };
  xhr.onload=function(){
    if(xhr.status===200){
      document.getElementById('ota-prog-txt').textContent='Готово! Перезагрузка (~15 сек)...';
      notify('Прошивка загружена! Перезагружаюсь...','');
      setTimeout(function(){window.location.reload();},16000);
    } else {
      notify('Ошибка загрузки: '+xhr.status,'err');
      document.getElementById('ota-btn').disabled=false;
    }
  };
  xhr.onerror=function(){notify('Ошибка сети','err');document.getElementById('ota-btn').disabled=false;};
  xhr.send(form);
}

// DnD OTA
(function(){
  var z=document.getElementById('ota-drop');
  if(!z)return;
  z.addEventListener('dragover',function(e){e.preventDefault();z.classList.add('drag');});
  z.addEventListener('dragleave',function(){z.classList.remove('drag');});
  z.addEventListener('drop',function(e){
    e.preventDefault();z.classList.remove('drag');
    var f=e.dataTransfer.files[0];
    if(f){document.getElementById('ota-file').files=e.dataTransfer.files;var inp=document.getElementById('ota-file');inp.files=e.dataTransfer.files;onFileSelected(inp);}
  });
})();

function closeModal(id){document.getElementById(id).classList.remove('show');}
document.addEventListener('keydown',function(e){if(e.key==='Escape')document.querySelectorAll('.modal-bg.show').forEach(function(m){m.classList.remove('show');});});
document.querySelectorAll('.modal-bg').forEach(function(bg){bg.addEventListener('click',function(e){if(e.target===bg)bg.classList.remove('show');});});

fetchStatus();fetchData();
setInterval(fetchData,2000);
setInterval(fetchStatus,10000);
</script>
</body>
</html>
)rawliteral";

// =====================================================================
// СТРАНИЦА НАСТРОЙКИ WI-FI (упрощённая в терминальном стиле)
// =====================================================================
const char wifi_config_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>SmartFan — Wi-Fi Config</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@700;900&display=swap');
  *{margin:0;padding:0;box-sizing:border-box}
  :root{--green:#00ff88;--cyan:#00e5ff;--orange:#ff9500;--red:#ff3b5c;--bg:#060a0f;--panel:#0c1420;--border:#0e2030;--text:#b0c8d8;--dim:#3a5568}
  body{background:var(--bg);color:var(--text);font-family:'Share Tech Mono',monospace;min-height:100vh;display:flex;align-items:flex-start;justify-content:center;padding:24px 16px 48px}
  .page{width:100%;max-width:520px}
  .logo{font-family:'Orbitron',monospace;font-size:20px;font-weight:900;color:var(--cyan);letter-spacing:2px;margin-bottom:24px;text-align:center}
  .logo span{color:var(--green)}
  .panel{background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:22px;margin-bottom:14px;position:relative;overflow:hidden}
  .panel::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--cyan),transparent)}
  .panel-title{font-family:'Orbitron',monospace;font-size:10px;letter-spacing:2px;color:var(--dim);margin-bottom:16px}
  .panel-title::before{content:'// ';color:var(--cyan)}
  .stat-row{display:flex;justify-content:space-between;font-size:13px;padding:7px 0;border-bottom:1px solid rgba(255,255,255,.04)}
  .stat-row:last-child{border-bottom:none}
  .sk{color:var(--dim)}.sv{color:var(--text)}.sv.ok{color:var(--green)}.sv.warn{color:var(--orange)}
  .field-lbl{font-size:11px;color:var(--dim);letter-spacing:1px;margin-bottom:6px;margin-top:14px}
  .field-input,.field-sel{width:100%;padding:12px 14px;background:rgba(0,0,0,.5);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:'Share Tech Mono',monospace;font-size:15px;outline:none;transition:border .15s;-webkit-appearance:none}
  .field-input:focus,.field-sel:focus{border-color:var(--cyan)}
  .scan-list{max-height:180px;overflow-y:auto;margin:8px 0;border:1px solid var(--border);border-radius:4px;display:none}
  .scan-item{padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.04);cursor:pointer;font-size:13px;display:flex;justify-content:space-between;transition:background .15s}
  .scan-item:hover{background:rgba(0,229,255,.05)}
  .scan-item.selected{color:var(--cyan)}
  .scan-hint{font-size:11px;color:var(--dim);margin-top:4px}
  .btn-row{display:flex;gap:10px;margin-top:16px}
  .btn{flex:1;padding:13px;border:none;border-radius:4px;font-family:'Orbitron',monospace;font-size:12px;letter-spacing:1px;cursor:pointer;transition:all .15s;display:flex;align-items:center;justify-content:center;gap:8px}
  .btn-scan{background:rgba(0,229,255,.1);border:1px solid rgba(0,229,255,.3);color:var(--cyan)}
  .btn-scan:hover{background:rgba(0,229,255,.2)}
  .btn-save{background:rgba(0,255,136,.1);border:1px solid rgba(0,255,136,.3);color:var(--green)}
  .btn-save:hover{background:rgba(0,255,136,.2)}
  .btn-ap{background:rgba(255,149,0,.1);border:1px solid rgba(255,149,0,.3);color:var(--orange)}
  .btn-ap:hover{background:rgba(255,149,0,.2)}
  .back-link{display:inline-block;color:var(--dim);font-size:12px;text-decoration:none;margin-bottom:16px;transition:color .15s}
  .back-link:hover{color:var(--cyan)}
  .toast{display:none;padding:12px 16px;border-radius:4px;font-size:13px;margin-top:12px;border:1px solid var(--green);background:rgba(0,255,136,.08);color:var(--green)}
  .toast.err{border-color:var(--red);background:rgba(255,59,92,.08);color:var(--red)}
  .modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.85);display:none;align-items:center;justify-content:center;z-index:500;padding:20px}
  .modal-bg.show{display:flex}
  .modal{background:var(--panel);border:1px solid var(--cyan);border-radius:6px;padding:28px;max-width:400px;width:100%;text-align:center;box-shadow:0 0 40px rgba(0,229,255,.15)}
  .modal h2{font-family:'Orbitron',monospace;font-size:14px;color:var(--green);letter-spacing:2px;margin-bottom:16px}
  .modal-ip{font-family:'Orbitron',monospace;font-size:28px;font-weight:900;color:var(--cyan);margin:16px 0;text-shadow:0 0 20px rgba(0,229,255,.5)}
  .modal-open{padding:12px 24px;background:rgba(0,255,136,.1);border:1px solid var(--green);border-radius:4px;color:var(--green);font-family:'Orbitron',monospace;font-size:12px;cursor:pointer;margin-top:8px;transition:all .15s}
  .modal-open:hover{background:rgba(0,255,136,.2)}
  .modal-dismiss{display:block;margin-top:12px;background:none;border:none;color:var(--dim);font-size:12px;cursor:pointer;font-family:'Share Tech Mono',monospace}
  .spin{display:inline-block;width:12px;height:12px;border:2px solid rgba(255,255,255,.2);border-radius:50%;border-top-color:currentColor;animation:sp 1s linear infinite}
  @keyframes sp{to{transform:rotate(360deg)}}
</style>
</head>
<body>
<div class="page">
  <a href="/" class="back-link">← ГЛАВНАЯ</a>
  <div class="logo">SMART<span>FAN</span> WI-FI</div>

  <div class="panel">
    <div class="panel-title">ТЕКУЩИЙ СТАТУС</div>
    <div class="stat-row"><span class="sk">РЕЖИМ</span><span class="sv" id="mode-txt">—</span></div>
    <div class="stat-row"><span class="sk">SSID</span><span class="sv" id="ssid-txt">—</span></div>
    <div class="stat-row"><span class="sk">IP АДРЕС</span><span class="sv ok" id="cur-ip">—</span></div>
  </div>

  <div class="panel">
    <div class="panel-title">НАСТРОЙКА ПОДКЛЮЧЕНИЯ</div>
    <div class="field-lbl">СЕТЬ Wi-Fi</div>
    <input type="text" class="field-input" id="ssid-in" placeholder="Имя сети (SSID)">
    <div class="btn-row" style="margin-top:10px;margin-bottom:0">
      <button class="btn btn-scan" id="scan-btn" onclick="scanNetworks()">
        <span id="scan-lbl">↻ СКАНИРОВАТЬ</span>
        <span id="scan-spin" style="display:none"><div class="spin"></div></span>
      </button>
    </div>
    <div class="scan-list" id="scan-list"></div>
    <div class="scan-hint" id="scan-hint"></div>
    <div class="field-lbl">ПАРОЛЬ</div>
    <input type="password" class="field-input" id="pass-in" placeholder="Пароль сети">
    <div class="btn-row">
      <button class="btn btn-save" id="save-btn" onclick="saveWiFi()">
        <span id="save-lbl">✓ СОХРАНИТЬ</span>
        <span id="save-spin" style="display:none"><div class="spin"></div></span>
      </button>
      <button class="btn btn-ap" onclick="stayAP()">AP РЕЖИМ</button>
    </div>
    <div class="toast" id="toast"></div>
  </div>
</div>

<div class="modal-bg" id="ip-modal">
  <div class="modal">
    <h2>// ПОДКЛЮЧЕНО</h2>
    <div style="font-size:12px;color:var(--dim)">Новый IP-адрес устройства:</div>
    <div class="modal-ip" id="new-ip-val">—</div>
    <button class="modal-open" onclick="openNewIP()">↗ ОТКРЫТЬ ПАНЕЛЬ</button>
    <button class="modal-dismiss" onclick="document.getElementById('ip-modal').classList.remove('show')">закрыть</button>
  </div>
</div>

<script>
var newIP='';
function toast(msg,t){var el=document.getElementById('toast');el.textContent=msg;el.className='toast '+(t||'');el.style.display='block';clearTimeout(el._t);el._t=setTimeout(function(){el.style.display='none';},5000);}
function refreshStatus(){
  fetch('/wifi_status').then(function(r){return r.json();}).then(function(d){
    document.getElementById('cur-ip').textContent=d.ip;
    document.getElementById('ssid-txt').textContent=d.ssid||'—';
    document.getElementById('mode-txt').textContent=d.mode==='ap'?'ТОЧКА ДОСТУПА':'КЛИЕНТ';
    document.getElementById('mode-txt').className='sv '+(d.mode==='ap'?'warn':'ok');
  });
}
function scanNetworks(){
  document.getElementById('scan-lbl').style.display='none';document.getElementById('scan-spin').style.display='inline-block';document.getElementById('scan-btn').disabled=true;
  fetch('/scan').then(function(r){return r.json();}).then(function(d){
    var list=document.getElementById('scan-list');list.innerHTML='';list.style.display='block';
    if(d.networks&&d.networks.length){
      d.networks.forEach(function(n){
        var item=document.createElement('div');item.className='scan-item';
        item.innerHTML='<span>'+n.ssid+'</span><span class="scan-rssi">'+n.rssi+'dBm</span>';
        item.onclick=function(){document.getElementById('ssid-in').value=n.ssid;document.querySelectorAll('.scan-item').forEach(function(i){i.classList.remove('selected');});item.classList.add('selected');};
        list.appendChild(item);
      });
      document.getElementById('scan-hint').textContent='Найдено: '+d.networks.length;
    } else {list.innerHTML='<div class="scan-item" style="color:var(--dim)">Сети не найдены</div>';}
  }).finally(function(){document.getElementById('scan-lbl').style.display='inline';document.getElementById('scan-spin').style.display='none';document.getElementById('scan-btn').disabled=false;});
}
function saveWiFi(){
  var ssid=document.getElementById('ssid-in').value;var pass=document.getElementById('pass-in').value;
  if(!ssid){toast('Введите имя сети','err');return;}
  document.getElementById('save-lbl').style.display='none';document.getElementById('save-spin').style.display='inline-block';document.getElementById('save-btn').disabled=true;
  fetch('/savewifi?ssid='+encodeURIComponent(ssid)+'&password='+encodeURIComponent(pass))
    .then(function(){toast('Сохранено. Подключение...','');return fetch('/connect_wifi?ssid='+encodeURIComponent(ssid)+'&password='+encodeURIComponent(pass));})
    .then(function(){setTimeout(checkNewIP,3000);})
    .catch(function(){toast('Ошибка','err');})
    .finally(function(){document.getElementById('save-lbl').style.display='inline';document.getElementById('save-spin').style.display='none';document.getElementById('save-btn').disabled=false;});
}
function checkNewIP(){
  fetch('/show_ip').then(function(r){return r.json();}).then(function(d){
    if(d.ip&&d.ip!==''&&d.ip!=='0.0.0.0'&&d.connected){
      newIP=d.ip;document.getElementById('new-ip-val').textContent=newIP;
      document.getElementById('ip-modal').classList.add('show');
    } else setTimeout(checkNewIP,2000);
  }).catch(function(){setTimeout(checkNewIP,2000);});
}
function stayAP(){fetch('/start_ap').then(function(){toast('Режим AP активирован','');refreshStatus();});}
function openNewIP(){if(newIP)window.location.href='http://'+newIP;}
refreshStatus();
setInterval(refreshStatus,5000);
</script>
</body>
</html>
)rawliteral";

// =====================================================================
// EEPROM / WiFi CONFIG
// =====================================================================
void loadWiFiConfig(){
  EEPROM.begin(128);
  EEPROM.get(0, wifiConfig);
  EEPROM.end();
  if(wifiConfig.ssid[0]==0xFF||wifiConfig.ssid[0]==0x00){savedSSID="";savedPassword="";}
  else{savedSSID=String(wifiConfig.ssid);savedPassword=String(wifiConfig.password);}
  Serial.println("WiFi config loaded: SSID="+savedSSID);
}
void saveWiFiConfig(){
  EEPROM.begin(128);
  strncpy(wifiConfig.ssid,savedSSID.c_str(),31); wifiConfig.ssid[31]=0;
  strncpy(wifiConfig.password,savedPassword.c_str(),63); wifiConfig.password[63]=0;
  EEPROM.put(0,wifiConfig);
  EEPROM.commit();EEPROM.end();
}

// =====================================================================
// RELAY / FAN
// =====================================================================
void initializeRelays(){
  digitalWrite(RELAY1,RELAY_OFF);digitalWrite(RELAY2,RELAY_OFF);
  digitalWrite(RELAY3,RELAY_OFF);digitalWrite(RELAY4,RELAY_OFF);
}
void setFanSpeed(int speed){
  autoMode=(speed==4);
  if(speed==4){fanSpeed=speed;updateAutoMode();return;}
  fanSpeed=speed;
  digitalWrite(RELAY1,speed>=1?RELAY_ON:RELAY_OFF);
  digitalWrite(RELAY2,speed>=2?RELAY_ON:RELAY_OFF);
  digitalWrite(RELAY3,speed>=3?RELAY_ON:RELAY_OFF);
  digitalWrite(RELAY4,RELAY_OFF);
  Serial.println("Fan speed: "+String(speed));
}
int getAutoSpeedForTemperature(float temp){
  if(temp<tempThresholds[0])return 0;
  if(temp<tempThresholds[1])return 1;
  if(temp<tempThresholds[2])return 2;
  return 3;
}
void updateAutoMode(){
  if(!autoMode||dhtError)return;
  int ns=getAutoSpeedForTemperature(temperature);
  if(ns!=lastAutoSpeed){lastAutoSpeed=ns;setFanSpeed(ns);autoMode=true;Serial.println("Auto speed: "+String(ns));}
}

// =====================================================================
// IR
// =====================================================================
void handleIRCommand(uint64_t command){
  irCommandCount++;
  for(int i=0;i<buttonCount;i++){
    if((uint32_t)command==buttons[i].code||(uint64_t)command==buttons[i].code){
      lastIRCommand=buttons[i].name+" ("+buttons[i].function+")";
      lastIRTime=millis();
      Serial.println("IR: "+lastIRCommand);
      if(buttons[i].code==CH_BUTTON){startAccessPoint();return;}
      if(i<5)setFanSpeed(i);
      return;
    }
  }
}

// =====================================================================
// WIFI
// =====================================================================
void startAccessPoint(){
  WiFi.mode(WIFI_AP);
  WiFi.softAP("SmartFan_Config","12345678");
  apMode=true;
  currentIP=WiFi.softAPIP().toString();
  Serial.println("AP started: "+currentIP);
}
void connectToWiFi(){}
void checkWiFiConnection(){
  static unsigned long lastCheck=0;
  if(millis()-lastCheck<10000)return;
  lastCheck=millis();
  if(savedSSID.length()>0&&WiFi.status()!=WL_CONNECTED&&!connectionInProgress){
    Serial.println("Reconnecting...");
    WiFi.begin(savedSSID.c_str(),savedPassword.c_str());
  }
}

// =====================================================================
// TIMER
// =====================================================================
void startFanTimer(unsigned long seconds,int speed){
  fanTimer.active=true;fanTimer.endTime=millis()+seconds*1000;
  fanTimer.duration=seconds;fanTimer.targetSpeed=speed;
  fanTimer.lastUpdate=millis();fanTimer.stopRequested=false;
  setFanSpeed(speed);
  Serial.println("Timer started: "+String(seconds)+"s, spd="+String(speed));
}
void stopFanTimer(){
  fanTimer.active=false;fanTimer.duration=0;
  setFanSpeed(0);Serial.println("Timer stopped");
}
void updateFanTimer(){
  if(!fanTimer.active)return;
  if(fanTimer.stopRequested){stopFanTimer();return;}
  unsigned long now=millis();
  if(now-fanTimer.lastUpdate>=1000){
    fanTimer.lastUpdate=now;
    if(fanTimer.duration>0)fanTimer.duration--;
    else{stopFanTimer();return;}
  }
  if(now>=fanTimer.endTime)stopFanTimer();
}

// =====================================================================
// HTTP HANDLERS
// =====================================================================
void handleRoot(){server.send_P(200,"text/html; charset=UTF-8",root_html);}
void handleWifiConfig(){server.send_P(200,"text/html; charset=UTF-8",wifi_config_html);}

void handleWifiStatus(){
  StaticJsonDocument<200> doc;
  bool connected=(WiFi.status()==WL_CONNECTED);
  doc["connected"]=connected;
  doc["mode"]=apMode&&!connected?"ap":"sta";
  doc["ip"]=connected?WiFi.localIP().toString():WiFi.softAPIP().toString();
  doc["ssid"]=savedSSID;
  String r;serializeJson(doc,r);server.send(200,"application/json",r);
}

void handleStartAP(){
  startAccessPoint();
  server.send(200,"text/plain","OK");
}

void handleConnectWifi(){
  if(!server.hasArg("ssid")){server.send(400,"text/plain","Missing");return;}
  savedSSID=server.arg("ssid");savedPassword=server.arg("password");
  saveWiFiConfig();
  WiFi.mode(WIFI_AP_STA);
  WiFi.softAP("SmartFan_Config","12345678");
  WiFi.begin(savedSSID.c_str(),savedPassword.c_str());
  apMode=true;connectionInProgress=true;
  unsigned long t=millis();
  while(WiFi.status()!=WL_CONNECTED&&millis()-t<WIFI_TIMEOUT){delay(300);}
  connectionInProgress=false;
  server.send(200,"text/plain","OK");
}

void handleShowIP(){
  StaticJsonDocument<100> doc;
  bool c=(WiFi.status()==WL_CONNECTED);
  doc["connected"]=c;
  doc["ip"]=c?WiFi.localIP().toString():"";
  String r;serializeJson(doc,r);server.send(200,"application/json",r);
}

void handleScanNetworks(){
  int n=WiFi.scanNetworks();
  StaticJsonDocument<1024> doc;
  JsonArray nets=doc.createNestedArray("networks");
  for(int i=0;i<n;i++){
    String ssid=WiFi.SSID(i);if(ssid.length()==0)continue;
    bool dup=false;for(int j=0;j<i;j++){if(WiFi.SSID(j)==ssid){dup=true;break;}}
    if(!dup){JsonObject o=nets.createNestedObject();o["ssid"]=ssid;o["rssi"]=WiFi.RSSI(i);}
  }
  for(int i=0;i<(int)nets.size();i++)
    for(int j=i+1;j<(int)nets.size();j++)
      if(nets[i]["rssi"].as<int>()<nets[j]["rssi"].as<int>())
        {JsonObject t=nets[i];nets[i]=nets[j];nets[j]=t;}
  String r;serializeJson(doc,r);server.send(200,"application/json",r);
  WiFi.scanDelete();
}

void handleGetData(){
  StaticJsonDocument<768> doc;
  doc["temp"]=temperature;doc["hum"]=humidity;doc["dhtError"]=dhtError;
  doc["speed"]=fanSpeed;doc["autoMode"]=autoMode;doc["autoSpeed"]=lastAutoSpeed;
  doc["uptime"]=getUptimeString();doc["irCount"]=irCommandCount;
  doc["freeHeap"]=ESP.getFreeHeap();
  if(lastIRTime>0&&millis()-lastIRTime<5000)doc["lastIR"]=lastIRCommand;
  JsonArray thr=doc.createNestedArray("thresholds");
  thr.add(tempThresholds[0]);thr.add(tempThresholds[1]);thr.add(tempThresholds[2]);
  JsonObject tmr=doc.createNestedObject("timer");
  tmr["active"]=fanTimer.active;tmr["secondsLeft"]=fanTimer.duration;tmr["targetSpeed"]=fanTimer.targetSpeed;
  String r;serializeJson(doc,r);server.send(200,"application/json",r);
}

void handleSetSpeed(){
  if(!server.hasArg("speed")){server.send(400,"text/plain","Missing");return;}
  int spd=server.arg("speed").toInt();
  if(spd<0||spd>4){server.send(400,"text/plain","Invalid");return;}
  setFanSpeed(spd);
  StaticJsonDocument<200> doc;doc["success"]=true;
  const char* msgs[]={"Вентилятор выключен","Скорость 1 — Низкая","Скорость 2 — Средняя","Скорость 3 — Высокая","Авторежим включён"};
  doc["message"]=msgs[spd];
  String r;serializeJson(doc,r);server.send(200,"application/json",r);
}

void handleSetThresholds(){
  if(!server.hasArg("t1")||!server.hasArg("t2")||!server.hasArg("t3")){server.send(400,"text/plain","Missing");return;}
  float a=server.arg("t1").toFloat(),b=server.arg("t2").toFloat(),c=server.arg("t3").toFloat();
  if(a>=b||b>=c){server.send(400,"text/plain","Order error");return;}
  tempThresholds[0]=a;tempThresholds[1]=b;tempThresholds[2]=c;
  if(autoMode)updateAutoMode();
  server.send(200,"text/plain","OK");
}

void handleSaveWifi(){
  if(!server.hasArg("ssid")||!server.hasArg("password")){server.send(400,"text/plain","Missing");return;}
  savedSSID=server.arg("ssid");savedPassword=server.arg("password");
  saveWiFiConfig();server.send(200,"text/plain","OK");
}

void handleSetTimer(){
  if(!server.hasArg("hours")||!server.hasArg("minutes")||!server.hasArg("seconds")||!server.hasArg("speed")){
    server.send(400,"application/json","{\"success\":false,\"message\":\"Нет параметров\"}");return;
  }
  int h=server.arg("hours").toInt(),m=server.arg("minutes").toInt();
  int s=server.arg("seconds").toInt(),spd=server.arg("speed").toInt();
  unsigned long tot=h*3600UL+m*60UL+s;
  if(tot==0){server.send(400,"application/json","{\"success\":false,\"message\":\"Время = 0\"}");return;}
  if(tot>86400){server.send(400,"application/json","{\"success\":false,\"message\":\"Макс 24 часа\"}");return;}
  if(spd<1||spd>4){server.send(400,"application/json","{\"success\":false,\"message\":\"Скорость 1-4\"}");return;}
  startFanTimer(tot,spd);
  StaticJsonDocument<200> doc;doc["success"]=true;
  String ts="";if(h>0)ts+=String(h)+"ч ";if(m>0)ts+=String(m)+"м ";if(s>0)ts+=String(s)+"с";
  doc["message"]="Таймер: "+ts;
  String r;serializeJson(doc,r);server.send(200,"application/json",r);
}

void handleStopTimer(){
  stopFanTimer();
  StaticJsonDocument<200> doc;doc["success"]=true;doc["message"]="Таймер остановлен";
  String r;serializeJson(doc,r);server.send(200,"application/json",r);
}

void handleNotFound(){
  if(apMode){server.sendHeader("Location","http://192.168.4.1/",true);server.send(302,"text/plain","");}
  else server.send(404,"text/plain","Not found");
}

String getUptimeString(){
  unsigned long s=millis()/1000,m=s/60,h=m/60,d=h/24;
  s%=60;m%=60;h%=24;
  String u="";if(d>0)u+=String(d)+"д ";if(h>0||d>0)u+=String(h)+"ч ";
  u+=String(m)+"м "+String(s)+"с";return u;
}

// =====================================================================
// SETUP
// =====================================================================
void setup(){
  Serial.begin(115200);delay(500);
  Serial.println("\n=== УМНЫЙ ВЕНТИЛЯТОР v3.0-TEST ===");
  Serial.println("=== ТЕСТОВАЯ ПРОШИВКА ДЛЯ ДЕМОНСТРАЦИИ OTA ===");

  loadWiFiConfig();

  pinMode(RELAY1,OUTPUT);pinMode(RELAY2,OUTPUT);
  pinMode(RELAY3,OUTPUT);pinMode(RELAY4,OUTPUT);
  initializeRelays();
  initialized=true;uptimeStart=millis();

  dht.begin();
  irrecv.enableIRIn();
  Serial.println("DHT22 + IR готовы");

  if(savedSSID.length()>0){
    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP("SmartFan_Config","12345678");
    WiFi.begin(savedSSID.c_str(),savedPassword.c_str());
    apMode=true;connectionInProgress=true;wifiStartTime=millis();
    while(WiFi.status()!=WL_CONNECTED&&millis()-wifiStartTime<WIFI_TIMEOUT){delay(500);Serial.print(".");}
    connectionInProgress=false;
    if(WiFi.status()==WL_CONNECTED){currentIP=WiFi.localIP().toString();Serial.println("\nSTA IP: "+currentIP);}
    else{currentIP=WiFi.softAPIP().toString();Serial.println("\nAP IP: "+currentIP);}
  } else {
    WiFi.mode(WIFI_AP);
    WiFi.softAP("SmartFan_Config","12345678");
    apMode=true;currentIP=WiFi.softAPIP().toString();
    Serial.println("AP IP: "+currentIP);
  }
  dnsServer.start(DNS_PORT,"*",WiFi.softAPIP());

  server.on("/",              handleRoot);
  server.on("/wifi_config",   handleWifiConfig);
  server.on("/wifi_status",   handleWifiStatus);
  server.on("/start_ap",      handleStartAP);
  server.on("/connect_wifi",  handleConnectWifi);
  server.on("/show_ip",       handleShowIP);
  server.on("/scan",          handleScanNetworks);
  server.on("/getData",       handleGetData);
  server.on("/setSpeed",      handleSetSpeed);
  server.on("/setThresholds", handleSetThresholds);
  server.on("/savewifi",      handleSaveWifi);
  server.on("/setTimer",      handleSetTimer);
  server.on("/stopTimer",     handleStopTimer);
  server.onNotFound(handleNotFound);

  httpUpdater.setup(&server);   // OTA через браузер: /update

  server.begin();
  Serial.println("HTTP сервер запущен");
  Serial.println("OTA (браузер): http://"+currentIP+"/update");

  // ArduinoOTA
  ArduinoOTA.setHostname("SmartFan-Test");
  ArduinoOTA.setPassword("12345678");
  ArduinoOTA.onStart([](){Serial.println("[OTA] Начало");setFanSpeed(0);});
  ArduinoOTA.onEnd([](){Serial.println("\n[OTA] Завершено");});
  ArduinoOTA.onProgress([](unsigned int p,unsigned int t){Serial.printf("[OTA] %u%%\r",p*100/t);});
  ArduinoOTA.onError([](ota_error_t e){Serial.printf("[OTA] Ошибка [%u]\n",e);});
  ArduinoOTA.begin();
  MDNS.begin("smartfan-test");
  Serial.println("[OTA] ArduinoOTA готов. Хост: SmartFan-Test");
}

// =====================================================================
// LOOP
// =====================================================================
void loop(){
  server.handleClient();
  ArduinoOTA.handle();
  MDNS.update();
  if(apMode) dnsServer.processNextRequest();
  checkWiFiConnection();

  static unsigned long lastDHT=0;
  if(millis()-lastDHT>2000){
    float h=dht.readHumidity(),t=dht.readTemperature();
    if(!isnan(h)&&!isnan(t)){temperature=t;humidity=h;dhtError=false;if(autoMode)updateAutoMode();}
    else dhtError=true;
    lastDHT=millis();
  }

  static unsigned long lastIR=0;
  if(millis()-lastIR>100){
    if(irrecv.decode(&results)){handleIRCommand(results.value);irrecv.resume();}
    lastIR=millis();
  }

  updateFanTimer();
}
