void handleRoot() {
  updateMinHeap();
  String s = pageStart(txt("status"));
  s += F("<h1 class='title'>⌂ "); s += txt("status"); s += F("</h1><div class='cards'>");
  s += F("<div class='card metric'><h3>WiFi</h3><div class='val'>"); s += WiFi.isConnected() ? txt("connected") : txt("notConnected"); s += F("</div><div class='small'>"); s += WiFi.isConnected()?WiFi.localIP().toString():String("-"); s += F(" "); s += rssiBars(); s += F("</div></div>");
  s += F("<div class='card metric'><h3>MQTT</h3><div class='val'>"); s += mqtt.connected()?txt("connected"):txt("notConnected"); s += F("</div><div class='small'>"); s += htmlEscape(lastMqttStatus); s += F("</div></div>");
  s += F("<div class='card metric'><h3>"); s += txt("arrival"); s += F("</h3><div class='val'>"); s += String(cfg.trackCount); s += F(" spor</div><div class='small'>"); s += htmlEscape(cfg.rowTopic); s += F("</div></div>");
  s += F("<div class='card metric'><h3>"); s += txt("freeheap"); s += F("</h3><div class='val'>"); s += String(ESP.getFreeHeap()); s += F("</div><div class='small'>Min: "); s += String(minFreeHeap == 0xFFFFFFFF ? ESP.getFreeHeap() : minFreeHeap); s += F("</div></div></div>");
  s += F("<div class='grid2'><div class='card'><h2>"); s += txt("arrival"); s += F("</h2><table class='tbl'><tr><th>Sp</th><th>Tog</th><th>Fra</th><th>Tid</th></tr>");
  for(uint8_t i=0;i<cfg.trackCount;i++){ s += F("<tr><td>"); s += String(i+1); s += F("</td><td>"); s += htmlEscape(trackTrain[i]); s += F("</td><td>"); s += htmlEscape(trackFrom[i]); s += F("</td><td>"); s += htmlEscape(trackTime[i]); s += F("</td></tr>"); }
  s += F("</table></div><div class='card'><h2>"); s += txt("latest"); s += F("</h2><table class='tbl'><tr><th>Topic</th><td>"); s += htmlEscape(lastPayloadTopic); s += F("</td></tr><tr><th>Payload</th><td>"); s += htmlEscape(lastPayloadValue); s += F("</td></tr><tr><th>Uptime</th><td>"); s += fmtUptime(); s += F("</td></tr></table><div class='actions'><a class='btn' href='/'>"); s += txt("updateStatus"); s += F("</a><a class='btn gray' href='/restart'>"); s += txt("restart"); s += F("</a></div></div></div>");
  s += pageEnd(); server.send(200, "text/html", s);
}

void handleArrival() {
  if (server.hasArg("save")) {
    strlcpySafe(cfg.boardName, server.arg("boardName"), sizeof(cfg.boardName));
    cfg.trackCount = constrain(server.arg("trackCount").toInt(), 1, MAX_TRACKS);
    strlcpySafe(cfg.rowTopic, server.arg("rowTopic"), sizeof(cfg.rowTopic));
    strlcpySafe(cfg.homeStation, server.arg("homeStation"), sizeof(cfg.homeStation));
    uint32_t sec = constrain(server.arg("displayInterval").toInt(), 1, 60);
    cfg.displayIntervalMs = sec * 1000UL;
    saveConfig();
    if (mqtt.connected()) { mqtt.disconnect(); }
    server.sendHeader("Location", "/arrival?saved=1"); server.send(303); return;
  }
  String s = pageStart(txt("arrival"));
  s += F("<h1 class='title'>▤ "); s += txt("arrival"); s += F("</h1>");
  if (server.hasArg("saved")) { s += F("<div class='card'><span class='pill ok'>"); s += txt("arrivalSaved"); s += F("</span></div>"); }
  s += F("<div class='card'><form method='get'><input type='hidden' name='save' value='1'>");
  s += F("<label>"); s += txt("boardName"); s += F("</label><input name='boardName' value='"); s += htmlEscape(cfg.boardName); s += F("'>");
  s += F("<label>"); s += txt("trackCount"); s += F("</label><input name='trackCount' type='number' min='1' max='6' value='"); s += String(cfg.trackCount); s += F("'>");
  s += F("<label>"); s += txt("homeStation"); s += F("</label><input name='homeStation' value='"); s += htmlEscape(cfg.homeStation); s += F("'>");
  s += F("<label>"); s += txt("rowTopic"); s += F("</label><input name='rowTopic' value='"); s += htmlEscape(cfg.rowTopic); s += F("'>");
  s += F("<label>"); s += txt("displayInterval"); s += F(" ("); s += txt("seconds"); s += F(")</label><input name='displayInterval' type='number' min='1' max='60' value='"); s += String(cfg.displayIntervalMs/1000); s += F("'>");
  s += F("<button>"); s += txt("save"); s += F("</button></form></div>");
  s += F("<div class='card'><h2>Payload</h2><p class='small'>Fra_Station;Fra_Spor;Til_Station;Til_Spor;Afgangstid;Ankomsttid;Tog</p><table class='tbl'>");
  s += F("<tr><th>Topic</th><td>"); s += htmlEscape(cfg.rowTopic); s += F("</td></tr>");
  s += F("<tr><th>Eksempel</th><td>OSL;2;HBF;4;14:10;14:15;IC123</td></tr>");
  s += F("</table></div>");
  s += pageEnd(); server.send(200, "text/html", s);
}

void handleConfig() {
  if (server.hasArg("lang")) { strlcpySafe(cfg.language, server.arg("lang"), sizeof(cfg.language)); saveConfig(); server.sendHeader("Location", "/config"); server.send(303); return; }
  if (server.hasArg("save")) {
    strlcpySafe(cfg.hostname, server.arg("hostname"), sizeof(cfg.hostname));
    strlcpySafe(cfg.language, server.arg("language"), sizeof(cfg.language));
    cfg.retained = server.hasArg("retained");
    saveConfig(); server.sendHeader("Location", "/config?saved=1"); server.send(303); return;
  }
  String s = pageStart(txt("system"));
  s += F("<h1 class='title'>⚙ "); s += txt("system"); s += F("</h1>");
  if (server.hasArg("saved")) { s += F("<div class='card'><span class='pill ok'>"); s += txt("saved"); s += F("</span></div>"); }
  s += F("<div class='card'><form method='get'><input type='hidden' name='save' value='1'><label>Hostname</label><input name='hostname' value='"); s += htmlEscape(cfg.hostname); s += F("'><label>"); s += txt("languageCode"); s += F("</label><select name='language'>");
  String l=currentLang(); String opts[3]={"DK","GB","D"}; for(uint8_t i=0;i<3;i++){ s += F("<option value='"); s += opts[i]; s += F("'"); if(l==opts[i]) s += F(" selected"); s += F(">"); s += opts[i]; s += F("</option>"); }
  s += F("</select><label><input style='width:auto' type='checkbox' name='retained' "); if(cfg.retained) s += F("checked"); s += F("> "); s += txt("retained"); s += F("</label><button>"); s += txt("save"); s += F("</button></form></div><div class='card'><div class='actions'><a class='btn gray' href='/restart'>"); s += txt("restart"); s += F("</a><a class='btn redbtn' href='/resetconfig'>"); s += txt("resetConfig"); s += F("</a></div></div>");
  s += pageEnd(); server.send(200,"text/html",s);
}

void handleWifi() {
  if (server.hasArg("save")) { strlcpySafe(cfg.wifiSsid, server.arg("ssid"), sizeof(cfg.wifiSsid)); strlcpySafe(cfg.wifiPassword, server.arg("pass"), sizeof(cfg.wifiPassword)); saveConfig(); server.send(200,"text/html",pageStart(txt("wifi"))+"<div class='card'>WiFi gemt. Genstarter...</div>"+pageEnd()); delay(1000); ESP.restart(); return; }
  int n = WiFi.scanNetworks();
  String s=pageStart(txt("wifi")); s += F("<h1 class='title'>≋ WiFi</h1><div class='card'><form method='get'><input type='hidden' name='save' value='1'><label>SSID</label><select name='ssid'>");
  for(int i=0;i<n;i++){ s += F("<option value='"); s += htmlEscape(WiFi.SSID(i)); s += F("'"); if(WiFi.SSID(i)==String(cfg.wifiSsid)) s += F(" selected"); s += F(">"); s += htmlEscape(WiFi.SSID(i)); s += F(" ("); s += String(WiFi.RSSI(i)); s += F(" dBm)</option>"); }
  s += F("</select><label>"); s += txt("password"); s += F("</label><input name='pass' type='password' value='"); s += htmlEscape(cfg.wifiPassword); s += F("'><button>"); s += txt("saveRestart"); s += F("</button></form></div>");
  s += pageEnd(); server.send(200,"text/html",s);
}

void handleMqtt() {
  if (server.hasArg("save")) { strlcpySafe(cfg.mqttHost, server.arg("host"), sizeof(cfg.mqttHost)); cfg.mqttPort=server.arg("port").toInt(); strlcpySafe(cfg.mqttUser, server.arg("user"), sizeof(cfg.mqttUser)); strlcpySafe(cfg.mqttPassword, server.arg("pass"), sizeof(cfg.mqttPassword)); cfg.mqttTls=server.hasArg("tls"); saveConfig(); setupMqtt(); if(mqtt.connected())mqtt.disconnect(); server.sendHeader("Location","/mqtt?saved=1"); server.send(303); return; }
  String s=pageStart(txt("mqtt")); s += F("<h1 class='title'>◇ MQTT</h1>"); if(server.hasArg("saved")){s += F("<div class='card'><span class='pill ok'>"); s += txt("mqttSaved"); s += F("</span></div>");}
  s += F("<div class='card'><form method='get'><input type='hidden' name='save' value='1'><label>"); s += txt("brokerHost"); s += F("</label><input name='host' value='"); s += htmlEscape(cfg.mqttHost); s += F("'><label>Port</label><input name='port' type='number' value='"); s += String(cfg.mqttPort); s += F("'><label>"); s += txt("user"); s += F("</label><input name='user' value='"); s += htmlEscape(cfg.mqttUser); s += F("'><label>"); s += txt("password"); s += F("</label><input name='pass' type='password' value='"); s += htmlEscape(cfg.mqttPassword); s += F("'><label><input style='width:auto' type='checkbox' name='tls' "); if(cfg.mqttTls)s+=F("checked"); s += F("> "); s += txt("useTls"); s += F("</label><button>"); s += txt("save"); s += F("</button></form></div>");
  s += F("<div class='card'><h2>MQTT Test</h2><p class='small'>Tester WiFi, forbindelse til broker og abonnement på topic.</p><div class='actions'><a class='btn' href='/mqtttest'>▣ Test MQTT-forbindelse</a></div></div>");
  s += pageEnd(); server.send(200,"text/html",s);
}


void handleMqttTest() {
  String resultClass = "bad";
  String resultTitle = "MQTT-forbindelse fejlede";
  String detail;

  if (!WiFi.isConnected()) {
    detail = "WiFi er ikke forbundet.";
  } else {
    setupMqtt();
    if (mqtt.connected()) mqtt.disconnect();
    delay(100);
    String id = String(cfg.hostname) + "-test-" + String(ESP.getChipId(), HEX);
    bool ok = strlen(cfg.mqttUser) > 0 ? mqtt.connect(id.c_str(), cfg.mqttUser, cfg.mqttPassword) : mqtt.connect(id.c_str());
    if (ok) {
      bool subOk = mqtt.subscribe(cfg.rowTopic);
      resultClass = subOk ? "ok" : "bad";
      resultTitle = subOk ? "MQTT-forbindelse OK" : "MQTT broker OK, men abonnement fejlede";
      detail = String("Broker: ") + cfg.mqttHost + ":" + String(cfg.mqttPort) + "<br>Topic: " + htmlEscape(String(cfg.rowTopic));
      lastMqttStatus = subOk ? "test ok" : "test subscribe failed";
      mqtt.disconnect();
    } else {
      detail = String("Broker: ") + cfg.mqttHost + ":" + String(cfg.mqttPort) + "<br>Fejlkode: " + String(mqtt.state());
      lastMqttStatus = "test failed rc=" + String(mqtt.state());
    }
  }

  String s = pageStart("MQTT Test");
  s += F("<h1 class='title'>◇ MQTT Test</h1><div class='card'><span class='pill "); s += resultClass; s += F("'>"); s += resultTitle; s += F("</span><p>"); s += detail; s += F("</p><div class='actions'><a class='btn' href='/mqtt'>Tilbage til MQTT</a><a class='btn gray' href='/'>Status</a></div></div>");
  s += pageEnd();
  server.send(200, "text/html", s);
}

void startWeb() {
  server.on("/icon.svg", [](){ server.send_P(200, "image/svg+xml", ICON_ARRIVAL); });
  server.on("/", handleRoot);
  server.on("/arrival", handleArrival);
  server.on("/config", handleConfig);
  server.on("/wifi", handleWifi);
  server.on("/mqtt", handleMqtt);
  server.on("/mqtttest", handleMqttTest);
  server.on("/restart", [](){ server.send(200,"text/html",pageStart(txt("restart"))+"<div class='card'>Genstarter...</div>"+pageEnd()); delay(1000); ESP.restart(); });
  server.on("/resetconfig", [](){ setDefaults(); saveConfig(); server.send(200,"text/html",pageStart("Reset")+"<div class='card'>Konfiguration nulstillet. Genstarter...</div>"+pageEnd()); delay(1000); ESP.restart(); });
  server.on("/debug", [](){ updateMinHeap(); String s=pageStart("Debug"); s += F("<h1 class='title'>Debug</h1><div class='card'><table class='tbl'><tr><th>Free heap</th><td>"); s += String(ESP.getFreeHeap()); s += F("</td></tr><tr><th>Min heap</th><td>"); s += String(minFreeHeap); s += F("</td></tr><tr><th>MQTT state</th><td>"); s += String(mqtt.state()); s += F("</td></tr></table></div>"); s += pageEnd(); server.send(200,"text/html",s); });
  server.on("/info", [](){ String s=pageStart(txt("info")); s += F("<h1 class='title'>ⓘ "); s += txt("info"); s += F("</h1><div class='card'><h2>NodeMCU Ankomsttavle</h2><p>"); s += txt("about"); s += F("</p><table class='tbl'><tr><th>Version</th><td>v"); s += APP_VERSION; s += F("</td></tr><tr><th>Jens</th><td>Jens Krogsgaard / Krogsgaards Modelbane</td></tr><tr><th>Platform</th><td>NodeMCU ESP8266</td></tr><tr><th>OLED</th><td>SSD1306 128x64, I2C D1/D2</td></tr></table></div>"); s += pageEnd(); server.send(200,"text/html",s); });
  server.on("/firmware", HTTP_GET, [](){ String s=pageStart(txt("firmware")); s += F("<h1 class='title'>↥ "); s += txt("firmware"); s += F("</h1><div class='card'><h2>"); s += txt("firmwareUpload"); s += F("</h2><p>"); s += txt("firmwareHelp"); s += F("</p><form method='POST' action='/firmware' enctype='multipart/form-data'><input type='file' name='update'><button>Update Firmware</button></form></div>"); s += pageEnd(); server.send(200,"text/html",s); });
  server.on("/firmware", HTTP_POST, [](){ server.sendHeader("Connection", "close"); server.send(200,"text/html",pageStart(txt("firmware"))+String(Update.hasError()?"<div class='card bad'>Firmware update fejlede.</div>":"<div class='card ok'>Firmware opdateret. Genstarter...</div>")+pageEnd()); delay(1000); ESP.restart(); }, [](){ HTTPUpload& upload=server.upload(); if(upload.status==UPLOAD_FILE_START){ WiFiUDP::stopAll(); Update.begin((ESP.getFreeSketchSpace()-0x1000)&0xFFFFF000); } else if(upload.status==UPLOAD_FILE_WRITE){ Update.write(upload.buf, upload.currentSize); } else if(upload.status==UPLOAD_FILE_END){ Update.end(true); } });
  server.onNotFound([](){ if(portalMode){ server.sendHeader("Location", String("http://") + WiFi.softAPIP().toString(), true); server.send(302,"text/plain",""); } else handleRoot(); });
  server.begin();
}
