/ src / web / handlers.rs
handlers.rs
   1  /// HTTP request handlers
   2  use anyhow::Result;
   3  use embedded_svc::io::Write;
   4  use log::{error, info, warn};
   5  use std::sync::Arc;
   6  use std::time::Duration;
   7  
   8  use super::state::{current_time_secs, format_uptime, WebServerState};
   9  use super::templates;
  10  use crate::hardware::backup;
  11  
  12  /// Redirect to admin panel
  13  pub fn redirect_to_admin(
  14      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
  15  ) -> Result<(), anyhow::Error> {
  16      let mut resp = req
  17          .into_response(302, None, &[("Location", "/admin")])
  18          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
  19      resp.write_all(b"")
  20          .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
  21      Ok(())
  22  }
  23  
  24  /// Display login page
  25  pub fn handle_login_page(
  26      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
  27      _state: &Arc<WebServerState>,
  28      error: Option<&str>,
  29  ) -> Result<(), anyhow::Error> {
  30      let error_html = if let Some(msg) = error {
  31          format!("<div class=\"error\">{}</div>", msg)
  32      } else {
  33          String::new()
  34      };
  35  
  36      let html = templates::LOGIN_PAGE.replace("{{ERROR}}", &error_html);
  37  
  38      let mut resp = req
  39          .into_ok_response()
  40          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
  41      resp.write_all(html.as_bytes())
  42          .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
  43      Ok(())
  44  }
  45  
  46  /// Handle login form submission
  47  pub fn handle_login_submit(
  48      mut req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
  49      state: &Arc<WebServerState>,
  50  ) -> Result<(), anyhow::Error> {
  51      let mut buf = vec![0u8; 512];
  52      let len = req
  53          .read(&mut buf)
  54          .map_err(|e| anyhow::anyhow!("Failed to read request: {:?}", e))?;
  55      let body = std::str::from_utf8(&buf[..len])?;
  56  
  57      let mut username = "";
  58      let mut password = "";
  59  
  60      for part in body.split('&') {
  61          if let Some((key, value)) = part.split_once('=') {
  62              let decoded = urlencoding::decode(value).unwrap_or_default();
  63              match key {
  64                  "username" => username = Box::leak(decoded.into_boxed_str()),
  65                  "password" => password = Box::leak(decoded.into_boxed_str()),
  66                  _ => {}
  67              }
  68          }
  69      }
  70  
  71      // Constant-time comparison to prevent timing attacks
  72      if username == state.admin_user && password == state.admin_pass {
  73          state.session.lock().unwrap().authenticate();
  74          info!("User '{}' authenticated successfully", username);
  75  
  76          let mut resp = req
  77              .into_response(302, None, &[("Location", "/admin")])
  78              .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
  79          resp.write_all(b"")
  80              .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
  81      } else {
  82          warn!("Failed login attempt for user '{}'", username);
  83          handle_login_page(req, state, Some("Invalid username or password"))?;
  84      }
  85  
  86      Ok(())
  87  }
  88  
  89  /// Display admin panel
  90  pub fn handle_admin_panel(
  91      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
  92      state: &Arc<WebServerState>,
  93      success_msg: Option<&str>,
  94  ) -> Result<(), anyhow::Error> {
  95      // Check authentication
  96      {
  97          let mut session = state.session.lock().unwrap();
  98          if !session.is_valid() {
  99              return send_unauthorized(req);
 100          }
 101          session.update_activity();
 102      }
 103  
 104      let settings = state.read_settings();
 105  
 106      // Get system info
 107      let uptime_secs = current_time_secs();
 108      let uptime_str = format_uptime(uptime_secs);
 109      let free_heap = unsafe { esp_idf_svc::sys::esp_get_free_heap_size() } / 1024;
 110  
 111      // Get relay controller and device states
 112      let relay_controller = state.get_relay_controller();
 113      let devices = relay_controller.channels();
 114      let device_cards = if !devices.is_empty() {
 115          devices
 116              .iter()
 117              .map(|ch| {
 118                  let status = if ch.is_on() { "ON" } else { "OFF" };
 119                  let class = if ch.is_on() { "on" } else { "off" };
 120                  format!(
 121                      "<div class='device-card'><h3>{}</h3><div class='status {}'>{}</div></div>",
 122                      ch.name(),
 123                      class,
 124                      status
 125                  )
 126              })
 127              .collect::<Vec<_>>()
 128              .join("\n")
 129      } else {
 130          "<p>No devices configured</p>".to_string()
 131      };
 132  
 133      let success_html = if let Some(msg) = success_msg {
 134          format!("<div class=\"success\">✓ {}</div>", msg)
 135      } else {
 136          String::new()
 137      };
 138  
 139      let html = templates::ADMIN_PANEL
 140          .replace("{{SUCCESS}}", &success_html)
 141          .replace("{{UPTIME}}", &uptime_str)
 142          .replace("{{FREE_HEAP}}", &free_heap.to_string())
 143          .replace("{{IP_ADDRESS}}", "N/A")
 144          .replace("{{RSSI}}", "N/A")
 145          .replace("{{DEVICE_CARDS}}", &device_cards)
 146          .replace("{{WIFI_SSID}}", &settings.wifi_ssid)
 147          .replace("{{WIFI_PASS}}", &settings.wifi_pass)
 148          .replace("{{MQTT_BROKER}}", &settings.mqtt_broker_url)
 149          .replace(
 150              "{{PUBLISH_INTERVAL}}",
 151              &settings.publish_interval_secs.to_string(),
 152          )
 153          .replace(
 154              "{{AUTO_ENABLED_CHECKED}}",
 155              if settings.automation_enabled {
 156                  "checked"
 157              } else {
 158                  ""
 159              },
 160          )
 161          .replace(
 162              "{{TEMP_HIGH_THRESHOLD}}",
 163              &settings.temp_high_threshold_c.to_string(),
 164          )
 165          .replace(
 166              "{{TEMP_LOW_THRESHOLD}}",
 167              &settings.temp_low_threshold_c.to_string(),
 168          )
 169          .replace(
 170              "{{HUMIDITY_HIGH_THRESHOLD}}",
 171              &settings.humidity_high_threshold.to_string(),
 172          )
 173          .replace(
 174              "{{GAS_THRESHOLD}}",
 175              &settings.gas_threshold_kohm.to_string(),
 176          )
 177          .replace("{{OTA_UPDATE_URL}}", &settings.ota_update_url)
 178          .replace(
 179              "{{OTA_AUTO_UPDATE_CHECKED}}",
 180              if settings.ota_auto_update {
 181                  "checked"
 182              } else {
 183                  ""
 184              },
 185          )
 186          .replace(
 187              "{{FIRMWARE_VERSION}}",
 188              &state.ota_manager.get_current_version(),
 189          )
 190          .replace(
 191              "{{PARTITION_RUNNING}}",
 192              &state
 193                  .ota_manager
 194                  .get_partition_info()
 195                  .map(|i| i.running)
 196                  .unwrap_or_else(|_| "unknown".to_string()),
 197          )
 198          .replace(
 199              "{{PARTITION_BOOT}}",
 200              &state
 201                  .ota_manager
 202                  .get_partition_info()
 203                  .map(|i| i.boot)
 204                  .unwrap_or_else(|_| "unknown".to_string()),
 205          )
 206          .replace(
 207              "{{PARTITION_NEXT}}",
 208              &state
 209                  .ota_manager
 210                  .get_partition_info()
 211                  .map(|i| i.next)
 212                  .unwrap_or_else(|_| "unknown".to_string()),
 213          )
 214          .replace(
 215              "{{ALARM_ENABLED_CHECKED}}",
 216              if settings.alarm_enabled {
 217                  "checked"
 218              } else {
 219                  ""
 220              },
 221          )
 222          .replace(
 223              "{{ALARM_WIFI_DISCONNECT_SECS}}",
 224              &settings.alarm_wifi_disconnect_secs.to_string(),
 225          )
 226          .replace(
 227              "{{ALARM_MQTT_FAILURE_SECS}}",
 228              &settings.alarm_mqtt_failure_secs.to_string(),
 229          )
 230          .replace(
 231              "{{ALARM_SENSOR_FAILURE_SECS}}",
 232              &settings.alarm_sensor_failure_secs.to_string(),
 233          )
 234          .replace(
 235              "{{ALARM_CRITICAL_TEMP_LOW}}",
 236              &settings.alarm_critical_temp_low.to_string(),
 237          )
 238          .replace(
 239              "{{ALARM_CRITICAL_TEMP_HIGH}}",
 240              &settings.alarm_critical_temp_high.to_string(),
 241          )
 242          .replace(
 243              "{{ALARM_AUTOMATION_DELAY_SECS}}",
 244              &settings.alarm_automation_delay_secs.to_string(),
 245          )
 246          .replace(
 247              "{{SYSLOG_ENABLED_CHECKED}}",
 248              if settings.syslog_enabled {
 249                  "checked"
 250              } else {
 251                  ""
 252              },
 253          )
 254          .replace("{{SYSLOG_SERVER}}", &settings.syslog_server)
 255          .replace("{{SYSLOG_PORT}}", &settings.syslog_port.to_string())
 256          .replace("{{APP_NAME}}", &settings.app_name)
 257          .replace("{{HOSTNAME}}", &settings.hostname)
 258          .replace("{{BACKUP_ENDPOINT}}", &settings.backup_endpoint);
 259  
 260      let mut resp = req
 261          .into_ok_response()
 262          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 263      resp.write_all(html.as_bytes())
 264          .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 265      Ok(())
 266  }
 267  
 268  /// Handle settings update
 269  pub fn handle_update_settings(
 270      mut req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 271      state: &Arc<WebServerState>,
 272  ) -> Result<(), anyhow::Error> {
 273      // Check authentication
 274      {
 275          let mut session = state.session.lock().unwrap();
 276          if !session.is_valid() {
 277              return send_unauthorized(req);
 278          }
 279          session.update_activity();
 280      }
 281  
 282      let mut buf = vec![0u8; 1024];
 283      let len = req
 284          .read(&mut buf)
 285          .map_err(|e| anyhow::anyhow!("Failed to read request: {:?}", e))?;
 286      let body = std::str::from_utf8(&buf[..len])?;
 287  
 288      let mut new_settings = state.read_settings().clone();
 289  
 290      // Reset checkboxes (they won't appear in form data if unchecked)
 291      new_settings.automation_enabled = false;
 292      new_settings.ota_auto_update = false;
 293      new_settings.alarm_enabled = false;
 294      new_settings.syslog_enabled = false;
 295  
 296      for part in body.split('&') {
 297          if let Some((key, value)) = part.split_once('=') {
 298              let decoded = urlencoding::decode(value).unwrap_or_default();
 299              match key {
 300                  "wifi_ssid" => new_settings.wifi_ssid = decoded.to_string(),
 301                  "wifi_pass" => {
 302                      if !decoded.is_empty() {
 303                          new_settings.wifi_pass = decoded.to_string();
 304                      }
 305                  }
 306                  "mqtt_broker" => new_settings.mqtt_broker_url = decoded.to_string(),
 307                  "publish_interval" => {
 308                      if let Ok(val) = decoded.parse::<i64>() {
 309                          new_settings.publish_interval_secs = val.clamp(5, 3600);
 310                      }
 311                  }
 312                  "automation_enabled" => new_settings.automation_enabled = decoded == "on",
 313                  "temp_high_threshold" => {
 314                      if let Ok(val) = decoded.parse::<f32>() {
 315                          new_settings.temp_high_threshold_c = val;
 316                      }
 317                  }
 318                  "temp_low_threshold" => {
 319                      if let Ok(val) = decoded.parse::<f32>() {
 320                          new_settings.temp_low_threshold_c = val;
 321                      }
 322                  }
 323                  "humidity_high_threshold" => {
 324                      if let Ok(val) = decoded.parse::<f32>() {
 325                          new_settings.humidity_high_threshold = val;
 326                      }
 327                  }
 328                  "gas_threshold" => {
 329                      if let Ok(val) = decoded.parse::<f32>() {
 330                          new_settings.gas_threshold_kohm = val;
 331                      }
 332                  }
 333                  "ota_update_url" => new_settings.ota_update_url = decoded.to_string(),
 334                  "ota_auto_update" => new_settings.ota_auto_update = decoded == "on",
 335                  "alarm_enabled" => new_settings.alarm_enabled = decoded == "on",
 336                  "alarm_wifi_disconnect_secs" => {
 337                      if let Ok(val) = decoded.parse::<i64>() {
 338                          new_settings.alarm_wifi_disconnect_secs = val.clamp(60, 3600);
 339                      }
 340                  }
 341                  "alarm_mqtt_failure_secs" => {
 342                      if let Ok(val) = decoded.parse::<i64>() {
 343                          new_settings.alarm_mqtt_failure_secs = val.clamp(60, 3600);
 344                      }
 345                  }
 346                  "alarm_sensor_failure_secs" => {
 347                      if let Ok(val) = decoded.parse::<i64>() {
 348                          new_settings.alarm_sensor_failure_secs = val.clamp(30, 600);
 349                      }
 350                  }
 351                  "alarm_critical_temp_low" => {
 352                      if let Ok(val) = decoded.parse::<f32>() {
 353                          new_settings.alarm_critical_temp_low = val.clamp(-20.0, 20.0);
 354                      }
 355                  }
 356                  "alarm_critical_temp_high" => {
 357                      if let Ok(val) = decoded.parse::<f32>() {
 358                          new_settings.alarm_critical_temp_high = val.clamp(30.0, 60.0);
 359                      }
 360                  }
 361                  "alarm_automation_delay_secs" => {
 362                      if let Ok(val) = decoded.parse::<i64>() {
 363                          new_settings.alarm_automation_delay_secs = val.clamp(60, 600);
 364                      }
 365                  }
 366                  "syslog_enabled" => new_settings.syslog_enabled = decoded == "on",
 367                  "syslog_server" => new_settings.syslog_server = decoded.to_string(),
 368                  "syslog_port" => {
 369                      if let Ok(val) = decoded.parse::<u16>() {
 370                          new_settings.syslog_port = val.clamp(1, 65535);
 371                      }
 372                  }
 373                  "app_name" => new_settings.app_name = decoded.to_string(),
 374                  "hostname" => new_settings.hostname = decoded.to_string(),
 375                  "backup_password" => {
 376                      if !decoded.is_empty() {
 377                          new_settings.backup_password = decoded.to_string();
 378                      }
 379                  }
 380                  "backup_endpoint" => new_settings.backup_endpoint = decoded.to_string(),
 381                  _ => {}
 382              }
 383          }
 384      }
 385  
 386      // Save to NVS
 387      if let Err(e) = new_settings.save(&state.nvs.lock().unwrap()) {
 388          error!("Failed to save settings: {:?}", e);
 389          handle_admin_panel(req, state, Some("Error saving settings"))?;
 390      } else {
 391          // Update OTA manager with new settings
 392          state
 393              .ota_manager
 394              .set_update_url(new_settings.ota_update_url.clone());
 395          state
 396              .ota_manager
 397              .set_auto_update(new_settings.ota_auto_update);
 398  
 399          *state.write_settings() = new_settings.clone();
 400  
 401          // Trigger automatic backup if configured
 402          if !new_settings.backup_password.is_empty() && !new_settings.backup_endpoint.is_empty() {
 403              info!("Triggering automatic backup after settings save...");
 404              if let Err(e) = backup::auto_backup_settings(&new_settings) {
 405                  error!("Automatic backup failed: {:?}", e);
 406                  // Continue anyway - backup failure shouldn't prevent settings save
 407              }
 408          }
 409  
 410          info!("Settings updated successfully");
 411          handle_admin_panel(
 412              req,
 413              state,
 414              Some("Settings saved successfully! Reboot required for Wi-Fi changes."),
 415          )?;
 416      }
 417  
 418      Ok(())
 419  }
 420  
 421  /// Handle logout
 422  pub fn handle_logout(
 423      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 424      state: &Arc<WebServerState>,
 425  ) -> Result<(), anyhow::Error> {
 426      state.session.lock().unwrap().logout();
 427      info!("User logged out");
 428  
 429      let mut resp = req
 430          .into_response(302, None, &[("Location", "/login")])
 431          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 432      resp.write_all(b"")
 433          .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 434      Ok(())
 435  }
 436  
 437  /// Handle reboot request
 438  pub fn handle_reboot(
 439      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 440      state: &Arc<WebServerState>,
 441  ) -> Result<(), anyhow::Error> {
 442      // Check authentication
 443      {
 444          let session = state.session.lock().unwrap();
 445          if !session.is_valid() {
 446              return send_unauthorized(req);
 447          }
 448      }
 449  
 450      info!("Reboot requested via web interface");
 451  
 452      let mut resp = req
 453          .into_ok_response()
 454          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 455      resp.write_all(
 456          b"<html><body><h1>Rebooting...</h1><p>Device will restart in 2 seconds.</p></body></html>",
 457      )
 458      .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 459  
 460      // Schedule reboot in separate thread
 461      std::thread::spawn(|| {
 462          std::thread::sleep(Duration::from_secs(2));
 463          unsafe { esp_idf_svc::sys::esp_restart() };
 464      });
 465  
 466      Ok(())
 467  }
 468  
 469  /// Handle relay control (deprecated - use device-specific endpoints)
 470  pub fn handle_relay_control(
 471      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 472      state: &Arc<WebServerState>,
 473      _turn_on: bool,
 474  ) -> Result<(), anyhow::Error> {
 475      // Check authentication
 476      {
 477          let session = state.session.lock().unwrap();
 478          if !session.is_valid() {
 479              return send_unauthorized(req);
 480          }
 481      }
 482  
 483      // Deprecated endpoint - redirect to admin panel
 484      info!("Legacy relay endpoint accessed - use device-specific endpoints");
 485      let mut resp = req
 486          .into_response(302, None, &[("Location", "/admin")])
 487          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 488      resp.write_all(b"")
 489          .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 490      Ok(())
 491  }
 492  
 493  /// Send 401 Unauthorized response
 494  pub fn send_unauthorized(
 495      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 496  ) -> Result<(), anyhow::Error> {
 497      let mut resp = req
 498          .into_response(401, Some("Unauthorized"), &[])
 499          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 500      resp.write_all(templates::ERROR_401.as_bytes())
 501          .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 502      Ok(())
 503  }
 504  
 505  /// Handle OTA update check and install
 506  pub fn handle_ota_check(
 507      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 508      state: &Arc<WebServerState>,
 509  ) -> Result<(), anyhow::Error> {
 510      info!("OTA update check requested");
 511  
 512      let result = state.ota_manager.check_and_update();
 513  
 514      let html = match result {
 515          Ok(true) => {
 516              info!("OTA update successful - device will reboot");
 517              templates::simple_message(
 518                  "Update Successful",
 519                  "Firmware updated successfully. Device is rebooting...",
 520              )
 521          }
 522          Ok(false) => {
 523              info!("No update available");
 524              templates::simple_message(
 525                  "No Update",
 526                  "No firmware update available. Device is up to date.",
 527              )
 528          }
 529          Err(e) => {
 530              error!("OTA update failed: {:?}", e);
 531              templates::simple_message(
 532                  "Update Failed",
 533                  &format!("Failed to update firmware: {:?}", e),
 534              )
 535          }
 536      };
 537  
 538      let mut resp = req
 539          .into_ok_response()
 540          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 541      resp.write_all(html.as_bytes())
 542          .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 543      Ok(())
 544  }
 545  
 546  /// Handle OTA firmware upload
 547  pub fn handle_ota_upload(
 548      mut req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 549      state: &Arc<WebServerState>,
 550  ) -> Result<(), anyhow::Error> {
 551      info!("OTA firmware upload requested");
 552  
 553      // Read the uploaded firmware data
 554      let mut firmware_data = Vec::new();
 555      let mut buf = [0u8; 4096];
 556  
 557      loop {
 558          match req.read(&mut buf) {
 559              Ok(0) => break,
 560              Ok(size) => firmware_data.extend_from_slice(&buf[..size]),
 561              Err(e) => {
 562                  error!("Failed to read upload data: {:?}", e);
 563                  let html =
 564                      templates::simple_message("Upload Failed", "Failed to read uploaded file");
 565                  let mut resp = req
 566                      .into_ok_response()
 567                      .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 568                  resp.write_all(html.as_bytes())
 569                      .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 570                  return Ok(());
 571              }
 572          }
 573      }
 574  
 575      info!("Received firmware upload: {} bytes", firmware_data.len());
 576  
 577      // Install the firmware
 578      let result = state.ota_manager.install_from_data(&firmware_data);
 579  
 580      let html = match result {
 581          Ok(_) => {
 582              info!("OTA upload successful - device will reboot");
 583              // Schedule reboot
 584              std::thread::spawn(|| {
 585                  std::thread::sleep(Duration::from_secs(2));
 586                  unsafe {
 587                      esp_idf_svc::sys::esp_restart();
 588                  }
 589              });
 590              templates::simple_message(
 591                  "Upload Successful",
 592                  "Firmware uploaded successfully. Device is rebooting...",
 593              )
 594          }
 595          Err(e) => {
 596              error!("OTA upload failed: {:?}", e);
 597              templates::simple_message(
 598                  "Upload Failed",
 599                  &format!("Failed to install firmware: {:?}", e),
 600              )
 601          }
 602      };
 603  
 604      let mut resp = req
 605          .into_ok_response()
 606          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 607      resp.write_all(html.as_bytes())
 608          .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 609      Ok(())
 610  }
 611  
 612  /// Handle live statistics API request (JSON response)
 613  pub fn handle_stats_api(
 614      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 615      state: &Arc<WebServerState>,
 616  ) -> Result<(), anyhow::Error> {
 617      // Check authentication
 618      {
 619          let mut session = state.session.lock().unwrap();
 620          if !session.is_valid() {
 621              let mut resp = req
 622                  .into_response(
 623                      401,
 624                      Some("Unauthorized"),
 625                      &[("Content-Type", "application/json")],
 626                  )
 627                  .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 628              resp.write_all(b"{\"error\":\"unauthorized\"}")
 629                  .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 630              return Ok(());
 631          }
 632          session.update_activity();
 633      }
 634  
 635      // Gather system metrics
 636      let metrics = state.get_live_metrics();
 637      let uptime = current_time_secs();
 638      let free_heap = unsafe { esp_idf_svc::sys::esp_get_free_heap_size() } / 1024;
 639      let min_free_heap = unsafe { esp_idf_svc::sys::esp_get_minimum_free_heap_size() } / 1024;
 640  
 641      // Device states
 642      let relay = state.get_relay_controller();
 643      let exhaust_state = relay
 644          .get_channel(crate::hardware::DeviceType::ExhaustFan)
 645          .map(|ch| ch.is_on())
 646          .unwrap_or(false);
 647      let circ_state = relay
 648          .get_channel(crate::hardware::DeviceType::CirculationFan)
 649          .map(|ch| ch.is_on())
 650          .unwrap_or(false);
 651      let heater_state = relay
 652          .get_channel(crate::hardware::DeviceType::Heater)
 653          .map(|ch| ch.is_on())
 654          .unwrap_or(false);
 655      let purifier_state = relay
 656          .get_channel(crate::hardware::DeviceType::AirPurifier)
 657          .map(|ch| ch.is_on())
 658          .unwrap_or(false);
 659  
 660      // Alarm status
 661      let alarm_active = state.alarm.as_ref().map(|a| a.is_active()).unwrap_or(false);
 662      let alarm_conditions: Vec<String> = state
 663          .alarm
 664          .as_ref()
 665          .map(|a| {
 666              a.get_active_conditions()
 667                  .iter()
 668                  .map(|c| format!("\"{}\"", c.description()))
 669                  .collect()
 670          })
 671          .unwrap_or_default();
 672      let alarm_conditions_json = alarm_conditions.join(",");
 673  
 674      // Build JSON response (manual to avoid large dependencies)
 675      let json = format!(
 676          r#"{{"uptime":{},
 677  "free_heap":{},
 678  "min_free_heap":{},
 679  "temperature":{:.1},
 680  "humidity":{:.1},
 681  "pressure":{:.1},
 682  "gas_resistance":{:.1},
 683  "timestamp":{},
 684  "devices":{{
 685  "exhaust_fan":{},
 686  "circulation_fan":{},
 687  "heater":{},
 688  "air_purifier":{}
 689  }},
 690  "alarm":{{
 691  "active":{},
 692  "conditions":[{}]
 693  }}}}"#,
 694          uptime,
 695          free_heap,
 696          min_free_heap,
 697          metrics.temperature,
 698          metrics.humidity,
 699          metrics.pressure,
 700          metrics.gas_resistance,
 701          metrics.timestamp,
 702          exhaust_state,
 703          circ_state,
 704          heater_state,
 705          purifier_state,
 706          alarm_active,
 707          alarm_conditions_json
 708      );
 709  
 710      let mut resp = req
 711          .into_response(200, Some("OK"), &[("Content-Type", "application/json")])
 712          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 713      resp.write_all(json.as_bytes())
 714          .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 715      Ok(())
 716  }
 717  
 718  /// Handle alarm clear request
 719  pub fn handle_alarm_clear(
 720      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 721      state: &Arc<WebServerState>,
 722  ) -> Result<(), anyhow::Error> {
 723      // Check authentication
 724      {
 725          let session = state.session.lock().unwrap();
 726          if !session.is_valid() {
 727              return send_unauthorized(req);
 728          }
 729      }
 730  
 731      let result = if let Some(ref alarm) = state.alarm {
 732          match alarm.clear_all() {
 733              Ok(_) => {
 734                  info!("All alarm conditions cleared via web interface");
 735                  templates::simple_message(
 736                      "Alarm Cleared",
 737                      "All alarm conditions have been cleared.",
 738                  )
 739              }
 740              Err(e) => {
 741                  error!("Failed to clear alarm: {:?}", e);
 742                  templates::simple_message("Error", &format!("Failed to clear alarm: {:?}", e))
 743              }
 744          }
 745      } else {
 746          templates::simple_message("Not Available", "Alarm controller is not enabled.")
 747      };
 748  
 749      let mut resp = req
 750          .into_ok_response()
 751          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 752      resp.write_all(result.as_bytes())
 753          .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 754      Ok(())
 755  }
 756  
 757  /// Handle settings download as TOML file
 758  pub fn handle_download_settings(
 759      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 760      state: &Arc<WebServerState>,
 761  ) -> Result<(), anyhow::Error> {
 762      // Check authentication
 763      {
 764          let session = state.session.lock().unwrap();
 765          if !session.is_valid() {
 766              return send_unauthorized(req);
 767          }
 768      }
 769  
 770      let settings = state.read_settings();
 771  
 772      match backup::export_to_toml(&settings) {
 773          Ok(toml_content) => {
 774              let filename = format!("{}_settings.toml", settings.hostname);
 775              let headers = [
 776                  ("Content-Type", "application/toml"),
 777                  (
 778                      "Content-Disposition",
 779                      &format!("attachment; filename=\"{}\"", filename),
 780                  ),
 781              ];
 782  
 783              let mut resp = req
 784                  .into_response(200, Some("OK"), &headers)
 785                  .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 786              resp.write_all(toml_content.as_bytes())
 787                  .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 788  
 789              info!("Settings downloaded as TOML by admin");
 790              Ok(())
 791          }
 792          Err(e) => {
 793              error!("Failed to export settings: {:?}", e);
 794              let html = templates::simple_message(
 795                  "Export Failed",
 796                  &format!("Failed to export settings: {:?}", e),
 797              );
 798              let mut resp = req
 799                  .into_ok_response()
 800                  .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 801              resp.write_all(html.as_bytes())
 802                  .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 803              Ok(())
 804          }
 805      }
 806  }
 807  
 808  /// Handle settings upload and restore from TOML file
 809  pub fn handle_upload_settings(
 810      mut req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 811      state: &Arc<WebServerState>,
 812  ) -> Result<(), anyhow::Error> {
 813      // Check authentication
 814      {
 815          let mut session = state.session.lock().unwrap();
 816          if !session.is_valid() {
 817              return send_unauthorized(req);
 818          }
 819          session.update_activity();
 820      }
 821  
 822      // Read multipart form data
 823      let mut buf = vec![0u8; 8192]; // Larger buffer for file upload
 824      let len = req
 825          .read(&mut buf)
 826          .map_err(|e| anyhow::anyhow!("Failed to read request: {:?}", e))?;
 827  
 828      if len == 0 {
 829          let html =
 830              templates::simple_message("Upload Failed", "No data received. Please select a file.");
 831          let mut resp = req
 832              .into_ok_response()
 833              .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 834          resp.write_all(html.as_bytes())
 835              .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 836          return Ok(());
 837      }
 838  
 839      let body = &buf[..len];
 840  
 841      // Extract TOML content from multipart data
 842      let toml_content = if let Some(toml_data) = extract_file_from_multipart(body) {
 843          match std::str::from_utf8(toml_data) {
 844              Ok(s) => s,
 845              Err(e) => {
 846                  let html = templates::simple_message(
 847                      "Upload Failed",
 848                      &format!("Invalid UTF-8 in uploaded file: {:?}", e),
 849                  );
 850                  let mut resp = req
 851                      .into_ok_response()
 852                      .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 853                  resp.write_all(html.as_bytes())
 854                      .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 855                  return Ok(());
 856              }
 857          }
 858      } else {
 859          let html = templates::simple_message(
 860              "Upload Failed",
 861              "Could not extract settings from uploaded file.",
 862          );
 863          let mut resp = req
 864              .into_ok_response()
 865              .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 866          resp.write_all(html.as_bytes())
 867              .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 868          return Ok(());
 869      };
 870  
 871      // Import settings from TOML
 872      let current_settings = state.read_settings();
 873      match backup::import_from_toml(toml_content, &current_settings) {
 874          Ok(new_settings) => {
 875              // Save to NVS
 876              if let Err(e) = new_settings.save(&state.nvs.lock().unwrap()) {
 877                  error!("Failed to save imported settings: {:?}", e);
 878                  let html = templates::simple_message(
 879                      "Import Failed",
 880                      &format!("Failed to save settings: {:?}", e),
 881                  );
 882                  let mut resp = req
 883                      .into_ok_response()
 884                      .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 885                  resp.write_all(html.as_bytes())
 886                      .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 887              } else {
 888                  // Update OTA manager
 889                  state
 890                      .ota_manager
 891                      .set_update_url(new_settings.ota_update_url.clone());
 892                  state
 893                      .ota_manager
 894                      .set_auto_update(new_settings.ota_auto_update);
 895  
 896                  *state.write_settings() = new_settings;
 897                  info!("Settings imported successfully from uploaded file");
 898  
 899                  let html = templates::simple_message(
 900                      "Import Successful",
 901                      "Settings imported successfully! Reboot required for Wi-Fi changes to take effect.",
 902                  );
 903                  let mut resp = req
 904                      .into_ok_response()
 905                      .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 906                  resp.write_all(html.as_bytes())
 907                      .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 908              }
 909          }
 910          Err(e) => {
 911              error!("Failed to import settings: {:?}", e);
 912              let html = templates::simple_message(
 913                  "Import Failed",
 914                  &format!("Failed to parse TOML settings: {:?}", e),
 915              );
 916              let mut resp = req
 917                  .into_ok_response()
 918                  .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
 919              resp.write_all(html.as_bytes())
 920                  .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
 921          }
 922      }
 923  
 924      Ok(())
 925  }
 926  
 927  /// Extract file content from multipart/form-data
 928  fn extract_file_from_multipart(data: &[u8]) -> Option<&[u8]> {
 929      // Find the boundary
 930      let boundary_end = data.iter().position(|&b| b == b'\r')?;
 931      let boundary = &data[..boundary_end];
 932  
 933      // Find the start of file content (after headers)
 934      let mut pos = 0;
 935      let mut header_end = 0;
 936  
 937      for i in 0..data.len().saturating_sub(3) {
 938          if &data[i..i + 4] == b"\r\n\r\n" {
 939              header_end = i + 4;
 940              pos = header_end;
 941              break;
 942          }
 943      }
 944  
 945      if header_end == 0 {
 946          return None;
 947      }
 948  
 949      // Find the end boundary
 950      for i in pos..data.len().saturating_sub(boundary.len() + 2) {
 951          if &data[i..i + 2] == b"\r\n" && &data[i + 2..i + 2 + boundary.len()] == boundary {
 952              return Some(&data[pos..i]);
 953          }
 954      }
 955  
 956      None
 957  }
 958  
 959  /// URL encoding utilities (simple implementation for ESP32)
 960  mod urlencoding {
 961      pub fn decode(s: &str) -> Option<String> {
 962          let mut result = String::with_capacity(s.len());
 963          let mut chars = s.chars();
 964  
 965          while let Some(ch) = chars.next() {
 966              match ch {
 967                  '%' => {
 968                      let mut hex = String::new();
 969                      hex.push(chars.next()?);
 970                      hex.push(chars.next()?);
 971                      let byte = u8::from_str_radix(&hex, 16).ok()?;
 972                      result.push(byte as char);
 973                  }
 974                  '+' => result.push(' '),
 975                  _ => result.push(ch),
 976              }
 977          }
 978  
 979          Some(result)
 980      }
 981  }
 982  
 983  /// Handle OTA rollback request (API endpoint)
 984  pub fn handle_ota_rollback(
 985      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
 986      state: &Arc<WebServerState>,
 987  ) -> Result<(), anyhow::Error> {
 988      // Verify authentication
 989      {
 990          let mut session = state.session.lock().unwrap();
 991          if !session.is_valid() {
 992              return send_unauthorized(req);
 993          }
 994          session.update_activity();
 995      }
 996  
 997      info!("OTA rollback requested via web interface");
 998  
 999      let ota_manager = state.ota_manager.clone();
1000  
1001      // Check if rollback is possible
1002      if ota_manager.is_pending_validation() {
1003          // Current firmware is pending validation, can rollback
1004          let response = serde_json::json!({
1005              "status": "initiating_rollback",
1006              "message": "Rolling back to previous firmware. Device will reboot."
1007          });
1008  
1009          let json = serde_json::to_string(&response)?;
1010          let mut resp = req
1011              .into_ok_response()
1012              .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
1013          resp.write_all(json.as_bytes())
1014              .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
1015  
1016          // Initiate rollback (this will reboot the device)
1017          std::thread::spawn(move || {
1018              std::thread::sleep(Duration::from_millis(500));
1019              let _ = ota_manager.rollback_and_reboot();
1020          });
1021  
1022          Ok(())
1023      } else {
1024          // No rollback available
1025          let response = serde_json::json!({
1026              "status": "error",
1027              "message": "Rollback not available - current firmware already validated"
1028          });
1029  
1030          let json = serde_json::to_string(&response)?;
1031          let mut resp = req
1032              .into_status_response(400)
1033              .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
1034          resp.write_all(json.as_bytes())
1035              .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
1036          Ok(())
1037      }
1038  }
1039  
1040  /// Get device diagnostics and status
1041  pub fn handle_device_status(
1042      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
1043      state: &Arc<WebServerState>,
1044  ) -> Result<(), anyhow::Error> {
1045      // Verify authentication
1046      {
1047          let mut session = state.session.lock().unwrap();
1048          if !session.is_valid() {
1049              return send_unauthorized(req);
1050          }
1051          session.update_activity();
1052      }
1053  
1054      let ota_manager = &state.ota_manager;
1055      let partition_info = ota_manager.get_partition_info().ok();
1056  
1057      // Get memory info
1058      let (free_heap, min_heap) = unsafe {
1059          (
1060              esp_idf_svc::sys::esp_get_free_heap_size(),
1061              esp_idf_svc::sys::esp_get_minimum_free_heap_size(),
1062          )
1063      };
1064  
1065      let device_id = state.read_settings().device_id.clone();
1066  
1067      let response = serde_json::json!({
1068          "firmware": {
1069              "version": ota_manager.get_current_version(),
1070              "pending_validation": ota_manager.is_pending_validation(),
1071              "partition": {
1072                  "running": partition_info.as_ref().map(|p| &p.running),
1073                  "boot": partition_info.as_ref().map(|p| &p.boot),
1074                  "next": partition_info.as_ref().map(|p| &p.next),
1075              }
1076          },
1077          "memory": {
1078              "free_heap_bytes": free_heap,
1079              "minimum_heap_bytes": min_heap,
1080              "heap_usage_percent": ((free_heap as f32 / (free_heap + 100000) as f32) * 100.0) as u32,
1081          },
1082          "uptime_seconds": current_time_secs(),
1083          "device_id": device_id,
1084      });
1085  
1086      let json = serde_json::to_string_pretty(&response)?;
1087      let mut resp = req
1088          .into_ok_response()
1089          .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
1090      resp.write_all(json.as_bytes())
1091          .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
1092      Ok(())
1093  }
1094  
1095  /// Mark current firmware as valid (API endpoint)
1096  pub fn handle_ota_mark_valid(
1097      req: embedded_svc::http::server::Request<&mut impl embedded_svc::http::server::Connection>,
1098      state: &Arc<WebServerState>,
1099  ) -> Result<(), anyhow::Error> {
1100      // Verify authentication
1101      {
1102          let mut session = state.session.lock().unwrap();
1103          if !session.is_valid() {
1104              return send_unauthorized(req);
1105          }
1106          session.update_activity();
1107      }
1108  
1109      let ota_manager = &state.ota_manager;
1110  
1111      if ota_manager.is_pending_validation() {
1112          match ota_manager.mark_valid() {
1113              Ok(_) => {
1114                  let response = serde_json::json!({
1115                      "status": "success",
1116                      "message": "Firmware marked as valid - rollback prevented"
1117                  });
1118                  let json = serde_json::to_string(&response)?;
1119                  let mut resp = req
1120                      .into_ok_response()
1121                      .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
1122                  resp.write_all(json.as_bytes())
1123                      .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
1124                  Ok(())
1125              }
1126              Err(e) => {
1127                  let response = serde_json::json!({
1128                      "status": "error",
1129                      "message": format!("Failed to mark firmware as valid: {}", e)
1130                  });
1131                  let json = serde_json::to_string(&response)?;
1132                  let mut resp = req
1133                      .into_status_response(500)
1134                      .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
1135                  resp.write_all(json.as_bytes())
1136                      .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
1137                  Ok(())
1138              }
1139          }
1140      } else {
1141          let response = serde_json::json!({
1142              "status": "info",
1143              "message": "Firmware already validated"
1144          });
1145          let json = serde_json::to_string(&response)?;
1146          let mut resp = req
1147              .into_ok_response()
1148              .map_err(|e| anyhow::anyhow!("Failed to create response: {:?}", e))?;
1149          resp.write_all(json.as_bytes())
1150              .map_err(|e| anyhow::anyhow!("Failed to write response: {:?}", e))?;
1151          Ok(())
1152      }
1153  }