2025-10-15 14:27:21 +08:00
|
|
|
// 文件名: src/web/web_server.cc
|
|
|
|
|
#include "web_server.h"
|
2025-11-03 09:37:54 +08:00
|
|
|
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <sstream>
|
|
|
|
|
|
|
|
|
|
#include "config/config_manager.h"
|
2025-10-15 14:27:21 +08:00
|
|
|
#include "spdlog/spdlog.h"
|
|
|
|
|
|
2025-11-04 14:52:43 +08:00
|
|
|
WebServer::WebServer(SystemMonitor::SystemMonitor &monitor,
|
|
|
|
|
DeviceManager &deviceManager, LiveDataCache &liveDataCache,
|
|
|
|
|
AlarmService &alarm_service, uint16_t port)
|
|
|
|
|
: crow::Crow<crow::CORSHandler>(), m_monitor(monitor),
|
|
|
|
|
m_device_manager(deviceManager), m_live_data_cache(liveDataCache),
|
|
|
|
|
m_alarm_service(alarm_service), m_port(port) {
|
|
|
|
|
auto &cors = this->get_middleware<crow::CORSHandler>();
|
2025-11-03 09:37:54 +08:00
|
|
|
cors.global()
|
|
|
|
|
.origin("*")
|
|
|
|
|
.headers("Content-Type", "Authorization")
|
|
|
|
|
.methods("GET"_method, "POST"_method, "OPTIONS"_method);
|
|
|
|
|
|
|
|
|
|
this->loglevel(crow::LogLevel::Warning);
|
|
|
|
|
setup_routes();
|
2025-10-15 14:27:21 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-03 09:37:54 +08:00
|
|
|
WebServer::~WebServer() { stop(); }
|
|
|
|
|
|
2025-10-15 14:27:21 +08:00
|
|
|
void WebServer::start() {
|
2025-11-03 09:37:54 +08:00
|
|
|
if (m_thread.joinable()) {
|
|
|
|
|
spdlog::warn("Web server is already running.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
m_thread = std::thread([this]() {
|
|
|
|
|
spdlog::info("Starting Web server on port {}", m_port);
|
|
|
|
|
this->bindaddr("0.0.0.0").port(m_port).run();
|
|
|
|
|
spdlog::info("Web server has stopped.");
|
|
|
|
|
});
|
2025-10-15 14:27:21 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WebServer::stop() {
|
2025-11-03 09:37:54 +08:00
|
|
|
crow::Crow<crow::CORSHandler>::stop();
|
|
|
|
|
if (m_thread.joinable()) {
|
|
|
|
|
m_thread.join();
|
|
|
|
|
}
|
2025-10-15 14:27:21 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WebServer::setup_routes() {
|
2025-11-03 09:37:54 +08:00
|
|
|
CROW_ROUTE((*this), "/api/system/id").methods("GET"_method)([this] {
|
|
|
|
|
auto deviceID = ConfigManager::getInstance().getDeviceID();
|
|
|
|
|
crow::json::wvalue response;
|
|
|
|
|
response["deviceID"] = deviceID;
|
|
|
|
|
return response;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
CROW_ROUTE((*this), "/api/system/status").methods("GET"_method)([this] {
|
|
|
|
|
auto cpu_util = m_monitor.getCpuUtilization();
|
|
|
|
|
auto mem_info = m_monitor.getMemoryInfo();
|
|
|
|
|
|
|
|
|
|
crow::json::wvalue response;
|
|
|
|
|
response["cpu_usage_percentage"] = cpu_util.totalUsagePercentage;
|
|
|
|
|
response["memory_total_kb"] = mem_info.total_kb;
|
|
|
|
|
response["memory_free_kb"] = mem_info.available_kb;
|
|
|
|
|
response["memory_usage_percentage"] =
|
|
|
|
|
(mem_info.total_kb > 0)
|
|
|
|
|
? (1.0 -
|
|
|
|
|
static_cast<double>(mem_info.available_kb) / mem_info.total_kb) *
|
|
|
|
|
100.0
|
2025-10-15 14:27:21 +08:00
|
|
|
: 0.0;
|
2025-10-16 17:05:24 +08:00
|
|
|
|
2025-11-03 09:37:54 +08:00
|
|
|
return response;
|
|
|
|
|
});
|
2025-10-24 14:19:33 +08:00
|
|
|
|
2025-11-03 09:37:54 +08:00
|
|
|
CROW_ROUTE((*this), "/api/devices").methods("GET"_method)([this] {
|
|
|
|
|
auto devices_info = m_device_manager.get_all_device_info();
|
2025-10-16 17:05:24 +08:00
|
|
|
|
2025-11-03 09:37:54 +08:00
|
|
|
std::vector<crow::json::wvalue> devices_json;
|
2025-11-04 14:52:43 +08:00
|
|
|
for (const auto &info : devices_info) {
|
2025-11-03 09:37:54 +08:00
|
|
|
crow::json::wvalue device_obj;
|
2025-10-16 17:05:24 +08:00
|
|
|
|
2025-11-03 09:37:54 +08:00
|
|
|
device_obj["id"] = info.id;
|
|
|
|
|
device_obj["type"] = info.type;
|
|
|
|
|
device_obj["is_running"] = info.is_running;
|
2025-10-24 14:19:33 +08:00
|
|
|
|
2025-11-03 09:37:54 +08:00
|
|
|
crow::json::wvalue details_obj;
|
2025-11-04 14:52:43 +08:00
|
|
|
for (const auto &pair : info.connection_details) {
|
2025-11-03 09:37:54 +08:00
|
|
|
details_obj[pair.first] = pair.second;
|
|
|
|
|
}
|
|
|
|
|
device_obj["connection_details"] = std::move(details_obj);
|
2025-10-24 14:19:33 +08:00
|
|
|
|
2025-11-03 09:37:54 +08:00
|
|
|
devices_json.push_back(std::move(device_obj));
|
|
|
|
|
}
|
2025-11-04 17:51:37 +08:00
|
|
|
auto res = crow::response(crow::json::wvalue(devices_json));
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
2025-11-03 09:37:54 +08:00
|
|
|
});
|
2025-10-24 14:19:33 +08:00
|
|
|
|
2025-11-03 09:37:54 +08:00
|
|
|
CROW_ROUTE((*this), "/api/data/latest").methods("GET"_method)([this] {
|
|
|
|
|
auto latest_data_map = m_live_data_cache.get_all_data();
|
|
|
|
|
|
|
|
|
|
crow::json::wvalue response;
|
2025-11-04 14:52:43 +08:00
|
|
|
for (const auto &pair : latest_data_map) {
|
2025-11-03 09:37:54 +08:00
|
|
|
response[pair.first] = crow::json::load(pair.second);
|
|
|
|
|
}
|
2025-11-04 17:51:37 +08:00
|
|
|
// 1. 将 JSON 对象转换为 crow::response
|
|
|
|
|
auto res = crow::response(response);
|
|
|
|
|
// 2. 明确设置 Content-Type 头部
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
// 3. 返回 response 对象
|
|
|
|
|
return res;
|
2025-11-03 09:37:54 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
CROW_ROUTE((*this), "/api/alarms/active").methods("GET"_method)([this] {
|
|
|
|
|
try {
|
|
|
|
|
auto json_string = m_alarm_service.getActiveAlarmsJson().dump();
|
|
|
|
|
|
|
|
|
|
auto res = crow::response(200, json_string);
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
|
2025-11-04 14:52:43 +08:00
|
|
|
} catch (const std::exception &e) {
|
2025-11-03 09:37:54 +08:00
|
|
|
spdlog::error("Error processing /api/alarms/active: {}", e.what());
|
|
|
|
|
crow::json::wvalue error_resp;
|
|
|
|
|
error_resp["error"] = "Failed to retrieve active alarms.";
|
|
|
|
|
|
|
|
|
|
auto res = crow::response(500, error_resp.dump());
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
CROW_ROUTE((*this), "/api/alarms/history")
|
2025-11-04 14:52:43 +08:00
|
|
|
.methods("GET"_method)([this](const crow::request &req) {
|
2025-11-03 09:37:54 +08:00
|
|
|
int limit = 100;
|
2025-10-24 14:19:33 +08:00
|
|
|
if (req.url_params.get("limit")) {
|
2025-11-03 09:37:54 +08:00
|
|
|
try {
|
|
|
|
|
limit = std::stoi(req.url_params.get("limit"));
|
2025-11-04 14:52:43 +08:00
|
|
|
} catch (const std::exception &) { /* ignore invalid */
|
2025-11-03 09:37:54 +08:00
|
|
|
}
|
2025-10-24 14:19:33 +08:00
|
|
|
}
|
2025-11-04 14:52:43 +08:00
|
|
|
if (limit <= 0)
|
|
|
|
|
limit = 100;
|
2025-10-24 14:19:33 +08:00
|
|
|
|
|
|
|
|
try {
|
2025-11-03 09:37:54 +08:00
|
|
|
auto json_string = m_alarm_service.getAlarmHistoryJson(limit).dump();
|
2025-10-24 14:19:33 +08:00
|
|
|
|
2025-11-03 09:37:54 +08:00
|
|
|
auto res = crow::response(200, json_string);
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
2025-10-24 14:19:33 +08:00
|
|
|
|
2025-11-04 14:52:43 +08:00
|
|
|
} catch (const std::exception &e) {
|
2025-11-03 09:37:54 +08:00
|
|
|
spdlog::error("Error processing /api/alarms/history: {}", e.what());
|
|
|
|
|
crow::json::wvalue error_resp;
|
|
|
|
|
error_resp["error"] = "Failed to retrieve alarm history.";
|
2025-10-24 14:19:33 +08:00
|
|
|
|
2025-11-03 09:37:54 +08:00
|
|
|
auto res = crow::response(500, error_resp.dump());
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
2025-10-24 14:19:33 +08:00
|
|
|
}
|
2025-11-03 09:37:54 +08:00
|
|
|
});
|
2025-11-04 13:46:53 +08:00
|
|
|
|
|
|
|
|
CROW_ROUTE((*this), "/api/alarms/reload").methods("POST"_method)([this]() {
|
|
|
|
|
spdlog::info("Web API: Received request to reload alarm rules...");
|
|
|
|
|
|
|
|
|
|
bool success = m_alarm_service.reload_rules();
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
crow::json::wvalue response_json;
|
|
|
|
|
response_json["status"] = "success";
|
|
|
|
|
response_json["message"] = "Alarm rules reloaded successfully.";
|
|
|
|
|
|
|
|
|
|
auto res = crow::response(200, response_json.dump());
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
crow::json::wvalue error_json;
|
|
|
|
|
error_json["status"] = "error";
|
|
|
|
|
error_json["message"] =
|
|
|
|
|
"Failed to reload alarm rules. Check service logs for details.";
|
|
|
|
|
auto res = crow::response(500, error_json.dump());
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
CROW_ROUTE((*this), "/api/alarms/clear")
|
2025-11-04 14:52:43 +08:00
|
|
|
.methods("POST"_method)([this](const crow::request &req) {
|
2025-11-04 13:46:53 +08:00
|
|
|
crow::json::rvalue j_body;
|
|
|
|
|
try {
|
|
|
|
|
j_body = crow::json::load(req.body);
|
2025-11-04 14:52:43 +08:00
|
|
|
} catch (const std::exception &e) {
|
2025-11-04 13:46:53 +08:00
|
|
|
spdlog::warn("Failed to parse request body for /api/alarms/clear: {}",
|
|
|
|
|
e.what());
|
|
|
|
|
auto res = crow::response(400, "{\"error\":\"Invalid JSON body.\"}");
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!j_body.has("rule_id") || !j_body.has("device_id")) {
|
|
|
|
|
auto res = crow::response(
|
|
|
|
|
400, "{\"error\":\"Missing 'rule_id' or 'device_id'.\"}");
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string rule_id = j_body["rule_id"].s();
|
|
|
|
|
std::string device_id = j_body["device_id"].s();
|
|
|
|
|
|
|
|
|
|
bool success = m_alarm_service.manually_clear_alarm(rule_id, device_id);
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
auto res = crow::response(
|
|
|
|
|
200, "{\"status\":\"success\", \"message\":\"Alarm cleared.\"}");
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
} else {
|
2025-11-04 14:52:43 +08:00
|
|
|
auto res = crow::response(
|
|
|
|
|
404, "{\"status\":\"error\", \"message\":\"Alarm not "
|
|
|
|
|
"found or not active.\"}");
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
CROW_ROUTE((*this), "/api/alarms/config").methods("GET"_method)([this]() {
|
|
|
|
|
std::string rules_json_string = m_alarm_service.get_rules_as_json_string();
|
|
|
|
|
auto res = crow::response(200, rules_json_string);
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
CROW_ROUTE((*this), "/api/alarms/config")
|
|
|
|
|
.methods("POST"_method)([this](const crow::request &req) {
|
|
|
|
|
const std::string &new_rules_content = req.body;
|
|
|
|
|
|
|
|
|
|
bool success =
|
|
|
|
|
m_alarm_service.save_rules_from_json_string(new_rules_content);
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
crow::json::wvalue response_json;
|
|
|
|
|
response_json["status"] = "success";
|
|
|
|
|
response_json["message"] =
|
|
|
|
|
"Rules saved successfully. A reload is required to apply.";
|
|
|
|
|
|
|
|
|
|
auto res = crow::response(200, response_json.dump());
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
crow::json::wvalue error_json;
|
|
|
|
|
error_json["status"] = "error";
|
|
|
|
|
error_json["message"] =
|
|
|
|
|
"Failed to save rules. Invalid JSON format or server error.";
|
|
|
|
|
|
|
|
|
|
auto res = crow::response(400, error_json.dump());
|
2025-11-04 13:46:53 +08:00
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-11-04 17:51:37 +08:00
|
|
|
|
|
|
|
|
// --- [!! 新增路由: 设备配置管理 !!] ---
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief GET /api/devices/config
|
|
|
|
|
* 获取设备配置 (devices.json) 的原始 JSON 字符串
|
|
|
|
|
*/
|
|
|
|
|
CROW_ROUTE((*this), "/api/devices/config").methods("GET"_method)([this]() {
|
|
|
|
|
std::string rules_json_string =
|
|
|
|
|
m_device_manager.get_config_as_json_string();
|
|
|
|
|
auto res = crow::response(200, rules_json_string);
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief POST /api/devices/config
|
|
|
|
|
* 保存设备配置 (devices.json) 的原始 JSON 字符串
|
|
|
|
|
*/
|
|
|
|
|
CROW_ROUTE((*this), "/api/devices/config")
|
|
|
|
|
.methods("POST"_method)([this](const crow::request &req) {
|
|
|
|
|
const std::string &new_rules_content = req.body;
|
|
|
|
|
|
|
|
|
|
// save_config_from_json_string 内部包含 JSON 格式和 Schema 校验
|
|
|
|
|
bool success =
|
|
|
|
|
m_device_manager.save_config_from_json_string(new_rules_content);
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
crow::json::wvalue response_json;
|
|
|
|
|
response_json["status"] = "success";
|
|
|
|
|
response_json["message"] = "Device config saved successfully. A "
|
|
|
|
|
"reload is required to apply.";
|
|
|
|
|
|
|
|
|
|
auto res = crow::response(200, response_json.dump());
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
crow::json::wvalue error_json;
|
|
|
|
|
error_json["status"] = "error";
|
|
|
|
|
error_json["message"] = "Failed to save rules. Invalid JSON format "
|
|
|
|
|
"or schema. Check service logs.";
|
|
|
|
|
|
|
|
|
|
auto res = crow::response(400, error_json.dump());
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief POST /api/devices/reload
|
|
|
|
|
* 通知后端从磁盘重载 devices.json 并应用
|
|
|
|
|
*/
|
|
|
|
|
CROW_ROUTE((*this), "/api/devices/reload").methods("POST"_method)([this]() {
|
|
|
|
|
spdlog::info("Web API: Received request to reload device rules...");
|
|
|
|
|
|
|
|
|
|
bool success = m_device_manager.reload_config_from_file();
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
crow::json::wvalue response_json;
|
|
|
|
|
response_json["status"] = "success";
|
|
|
|
|
response_json["message"] = "Device rules reload posted successfully.";
|
|
|
|
|
|
|
|
|
|
auto res = crow::response(200, response_json.dump());
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
crow::json::wvalue error_json;
|
|
|
|
|
error_json["status"] = "error";
|
|
|
|
|
error_json["message"] =
|
|
|
|
|
"Failed to post device rules reload. Check service logs.";
|
|
|
|
|
auto res = crow::response(500, error_json.dump());
|
|
|
|
|
res.set_header("Content-Type", "application/json");
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-10-15 14:27:21 +08:00
|
|
|
}
|