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, ¤t_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 }