Forráskód Böngészése

feat: US-012 - Webserver Core - HTTP/WebSocket Server Setup

Implement HTTP server with WebSocket support using Boost.Beast:

- HTTP server using Boost.Beast library with async I/O
- REST API on configurable port (default 8080) with CLI, env, and YAML config
- WebSocket endpoint at configurable path (default /ws) with message handling
- Static file serving from webui/dist with SPA routing fallback
- Full CORS configuration for development (origins, methods, headers, credentials)
- Request logging with spdlog showing method, path, status code, and duration

Key components:
- HttpServer: Main server class managing connections and WebSocket sessions
- HttpSession: Handles individual HTTP connections with static/API routing
- WebSocketSession: Manages WebSocket connections with message queuing
- ConfigLoader: Loads config from YAML file, environment variables, and CLI
- YAML config file at config/smartbotic-server.yaml

Build system updates:
- Added Boost 1.86.0 dependency via FetchContent
- Created smartbotic_webserver static library
- Linked Beast, ASIO, yaml-cpp for HTTP/WebSocket and config support
Fszontagh 6 hónapja
szülő
commit
8e397f50f8

+ 17 - 0
CMakeLists.txt

@@ -1,4 +1,5 @@
 cmake_minimum_required(VERSION 3.22)
+cmake_policy(SET CMP0135 NEW)
 
 project(smartbotic-crm
     VERSION 0.1.0
@@ -77,6 +78,14 @@ FetchContent_Declare(
     GIT_TAG 1.0.19
 )
 
+# Boost - For Beast HTTP/WebSocket library
+FetchContent_Declare(
+    Boost
+    URL https://github.com/boostorg/boost/releases/download/boost-1.86.0/boost-1.86.0-cmake.tar.xz
+    URL_HASH SHA256=2c5ec5edcdff47ff55e27ed9560b0a0b94b07bd07ed9928b476150e16b0efc57
+    DOWNLOAD_EXTRACT_TIMESTAMP TRUE
+)
+
 # Make dependencies available
 message(STATUS "Fetching spdlog...")
 FetchContent_MakeAvailable(spdlog)
@@ -109,6 +118,14 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-pedantic -Wno-overflow -Wno-error")
 FetchContent_MakeAvailable(grpc)
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS_BACKUP}")
 
+message(STATUS "Fetching Boost...")
+set(BOOST_INCLUDE_LIBRARIES beast asio system)
+set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
+set(CMAKE_CXX_FLAGS_BACKUP2 "${CMAKE_CXX_FLAGS}")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error")
+FetchContent_MakeAvailable(Boost)
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS_BACKUP2}")
+
 # libsodium requires special handling as it uses autotools
 FetchContent_GetProperties(libsodium)
 if(NOT libsodium_POPULATED)

+ 44 - 0
config/smartbotic-server.yaml

@@ -0,0 +1,44 @@
+# SmartBotic Web Server Configuration
+# This file configures the HTTP/WebSocket server for serving the REST API and WebUI
+
+server:
+  # Address to bind to (0.0.0.0 for all interfaces)
+  address: "0.0.0.0"
+  # Port to listen on (default HTTP port 8080)
+  port: 8080
+  # Path to WebUI static files
+  webui_path: "./webui/dist"
+
+websocket:
+  # WebSocket endpoint path
+  path: "/ws"
+
+cors:
+  # Enable CORS headers (useful for development)
+  enabled: true
+  # Allowed origins (* for all)
+  allowed_origins:
+    - "*"
+  # Allowed HTTP methods
+  allowed_methods:
+    - "GET"
+    - "POST"
+    - "PUT"
+    - "DELETE"
+    - "OPTIONS"
+  # Allowed request headers
+  allowed_headers:
+    - "Content-Type"
+    - "Authorization"
+  # Allow credentials (cookies, auth headers)
+  allow_credentials: false
+  # Preflight response cache duration (seconds)
+  max_age: 86400
+
+logging:
+  # Log level: trace, debug, info, warn, error, critical, off
+  level: "info"
+
+database:
+  # Database gRPC service address
+  address: "127.0.0.1:50051"

+ 28 - 0
webserver/CMakeLists.txt

@@ -1,5 +1,32 @@
 # Web server - smartbotic-server binary
 
+# Create library for webserver components
+add_library(smartbotic_webserver STATIC
+    src/config.cpp
+    src/http_server.cpp
+)
+
+target_include_directories(smartbotic_webserver
+    PUBLIC
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+        $<INSTALL_INTERFACE:include>
+)
+
+target_compile_options(smartbotic_webserver PRIVATE ${SMARTBOTIC_CXX_WARNINGS})
+
+target_link_libraries(smartbotic_webserver
+    PUBLIC
+        smartbotic::common
+        spdlog::spdlog
+        nlohmann_json::nlohmann_json
+        yaml-cpp::yaml-cpp
+        Boost::beast
+        Boost::asio
+)
+
+add_library(smartbotic::webserver ALIAS smartbotic_webserver)
+
+# Create the executable
 add_executable(smartbotic-server
     src/main.cpp
 )
@@ -13,6 +40,7 @@ target_compile_options(smartbotic-server PRIVATE ${SMARTBOTIC_CXX_WARNINGS})
 
 target_link_libraries(smartbotic-server
     PRIVATE
+        smartbotic::webserver
         smartbotic::common
         smartbotic::proto
         spdlog::spdlog

+ 107 - 0
webserver/include/smartbotic/webserver/config.hpp

@@ -0,0 +1,107 @@
+#pragma once
+
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace smartbotic::webserver {
+
+/// Log level configuration
+enum class LogLevel : uint8_t {
+    kTrace,
+    kDebug,
+    kInfo,
+    kWarn,
+    kError,
+    kCritical,
+    kOff
+};
+
+/// Convert log level to string
+[[nodiscard]] auto LogLevelToString(LogLevel level) -> std::string;
+
+/// Parse log level from string (case-insensitive)
+[[nodiscard]] auto ParseLogLevel(const std::string& str) -> std::optional<LogLevel>;
+
+/// CORS configuration for development
+struct CorsConfig {
+    bool enabled = true;                               // Enable CORS
+    std::vector<std::string> allowed_origins = {"*"};  // Allowed origins
+    std::vector<std::string> allowed_methods = {"GET", "POST", "PUT", "DELETE", "OPTIONS"};
+    std::vector<std::string> allowed_headers = {"Content-Type", "Authorization"};
+    bool allow_credentials = false;  // Allow credentials
+    int max_age = 86400;             // Preflight cache duration (seconds)
+};
+
+/// Complete HTTP server configuration
+/// Supports loading from:
+/// 1. Config file (YAML) - smartbotic-server.yaml
+/// 2. Command-line arguments (override config file)
+/// 3. Environment variables
+struct HttpServerConfig {
+    // Server configuration
+    std::string address = "0.0.0.0";  // Listen address
+    uint16_t port = 8080;             // Listen port (default HTTP port)
+
+    // Static file serving
+    std::string webui_path = "./webui/dist";  // Path to WebUI static files
+
+    // WebSocket configuration
+    std::string ws_path = "/ws";  // WebSocket endpoint path
+
+    // CORS configuration
+    CorsConfig cors;
+
+    // Logging configuration
+    LogLevel log_level = LogLevel::kInfo;  // Default log level
+
+    // Database gRPC connection
+    std::string database_address = "127.0.0.1:50051";  // Database service address
+
+    /// Get the full address string (address:port)
+    [[nodiscard]] auto GetListenAddress() const -> std::string { return address + ":" + std::to_string(port); }
+
+    /// Create default configuration
+    [[nodiscard]] static auto Default() -> HttpServerConfig { return HttpServerConfig{}; }
+};
+
+/// Configuration loader that handles:
+/// - YAML config file parsing
+/// - Command-line argument parsing
+/// - Environment variable substitution
+class ConfigLoader {
+public:
+    /// Load configuration from all sources
+    /// Priority (highest to lowest):
+    /// 1. Command-line arguments
+    /// 2. Environment variables
+    /// 3. Config file
+    /// 4. Built-in defaults
+    [[nodiscard]] static auto Load(int argc, char* argv[]) -> HttpServerConfig;
+
+    /// Load configuration from a specific config file
+    [[nodiscard]] static auto LoadFromFile(const std::string& path) -> std::optional<HttpServerConfig>;
+
+    /// Parse command-line arguments into a config
+    /// Returns nullopt if help was requested
+    [[nodiscard]] static auto ParseCommandLine(int argc, char* argv[], HttpServerConfig& config) -> std::optional<bool>;
+
+    /// Apply environment variables to config
+    static void ApplyEnvironment(HttpServerConfig& config);
+
+    /// Get the default config file paths to search
+    [[nodiscard]] static auto GetDefaultConfigPaths() -> std::vector<std::string>;
+
+    /// Print help message
+    static void PrintHelp(const char* program_name);
+
+    /// Print current configuration
+    static void PrintConfig(const HttpServerConfig& config);
+
+private:
+    /// Find the first existing config file from the default paths
+    [[nodiscard]] static auto FindConfigFile() -> std::optional<std::string>;
+};
+
+}  // namespace smartbotic::webserver

+ 181 - 0
webserver/include/smartbotic/webserver/http_server.hpp

@@ -0,0 +1,181 @@
+#pragma once
+
+#include <atomic>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/strand.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/beast/websocket.hpp>
+#include <chrono>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <unordered_set>
+#include <vector>
+
+#include "smartbotic/webserver/config.hpp"
+
+namespace smartbotic::webserver {
+
+namespace beast = boost::beast;
+namespace http = beast::http;
+namespace websocket = beast::websocket;
+namespace asio = boost::asio;
+using tcp = asio::ip::tcp;
+
+// Forward declarations
+class HttpSession;
+class WebSocketSession;
+
+/// Callback type for WebSocket messages
+using WebSocketMessageHandler = std::function<void(WebSocketSession&, const std::string&)>;
+
+/// WebSocket session that handles a single WebSocket connection
+class WebSocketSession : public std::enable_shared_from_this<WebSocketSession> {
+public:
+    explicit WebSocketSession(tcp::socket socket, const HttpServerConfig& config);
+    ~WebSocketSession();
+
+    /// Non-copyable and non-movable
+    WebSocketSession(const WebSocketSession&) = delete;
+    auto operator=(const WebSocketSession&) -> WebSocketSession& = delete;
+    WebSocketSession(WebSocketSession&&) = delete;
+    auto operator=(WebSocketSession&&) -> WebSocketSession& = delete;
+
+    /// Start the WebSocket session
+    void Run(http::request<http::string_body> req);
+
+    /// Send a text message to the client
+    void Send(const std::string& message);
+
+    /// Close the WebSocket connection
+    void Close();
+
+    /// Check if the session is open
+    [[nodiscard]] auto IsOpen() const -> bool;
+
+    /// Set the message handler
+    void SetMessageHandler(WebSocketMessageHandler handler);
+
+private:
+    void DoAccept(http::request<http::string_body> req);
+    void OnAccept(beast::error_code ec);
+    void DoRead();
+    void OnRead(beast::error_code ec, std::size_t bytes_transferred);
+    void OnWrite(beast::error_code ec, std::size_t bytes_transferred);
+
+    websocket::stream<beast::tcp_stream> ws_;
+    beast::flat_buffer buffer_;
+    const HttpServerConfig& config_;
+    WebSocketMessageHandler message_handler_;
+    std::vector<std::shared_ptr<std::string const>> queue_;
+    std::mutex mutex_;
+};
+
+/// HTTP session that handles a single HTTP connection
+class HttpSession : public std::enable_shared_from_this<HttpSession> {
+public:
+    HttpSession(tcp::socket socket, const HttpServerConfig& config,
+                std::function<void(std::shared_ptr<WebSocketSession>)> ws_connect_handler,
+                std::function<void(std::shared_ptr<WebSocketSession>)> ws_disconnect_handler);
+    ~HttpSession();
+
+    /// Non-copyable and non-movable
+    HttpSession(const HttpSession&) = delete;
+    auto operator=(const HttpSession&) -> HttpSession& = delete;
+    HttpSession(HttpSession&&) = delete;
+    auto operator=(HttpSession&&) -> HttpSession& = delete;
+
+    /// Start processing HTTP requests
+    void Run();
+
+private:
+    void DoRead();
+    void OnRead(beast::error_code ec, std::size_t bytes_transferred);
+    void HandleRequest(http::request<http::string_body>&& req);
+    void SendResponse(http::response<http::string_body>&& res);
+    void OnWrite(beast::error_code ec, std::size_t bytes_transferred, bool close);
+    void LogRequest(http::status status);
+
+    [[nodiscard]] auto HandleStaticFile(const std::string& target) -> http::response<http::string_body>;
+    [[nodiscard]] auto HandleApiRequest(http::request<http::string_body>& req) -> http::response<http::string_body>;
+    [[nodiscard]] auto MakeErrorResponse(http::status status, const std::string& message)
+        -> http::response<http::string_body>;
+    void ApplyCorsHeaders(http::response<http::string_body>& res);
+    [[nodiscard]] auto GetMimeType(const std::string& path) -> std::string;
+
+    beast::tcp_stream stream_;
+    beast::flat_buffer buffer_;
+    http::request<http::string_body> req_;
+    const HttpServerConfig& config_;
+    std::function<void(std::shared_ptr<WebSocketSession>)> ws_connect_handler_;
+    std::function<void(std::shared_ptr<WebSocketSession>)> ws_disconnect_handler_;
+
+    // Request timing for logging
+    std::chrono::steady_clock::time_point request_start_;
+    std::string request_method_;
+    std::string request_target_;
+};
+
+/// Main HTTP server that hosts REST API and WebSocket endpoints
+class HttpServer {
+public:
+    /// Create a server with the given configuration
+    explicit HttpServer(HttpServerConfig config = HttpServerConfig::Default());
+
+    ~HttpServer();
+
+    /// Non-copyable and non-movable
+    HttpServer(const HttpServer&) = delete;
+    auto operator=(const HttpServer&) -> HttpServer& = delete;
+    HttpServer(HttpServer&&) = delete;
+    auto operator=(HttpServer&&) -> HttpServer& = delete;
+
+    /// Start the server (begins accepting requests)
+    /// Returns true on success, false on failure
+    [[nodiscard]] auto Start() -> bool;
+
+    /// Stop the server gracefully
+    void Stop();
+
+    /// Wait for the server to shutdown
+    void Wait();
+
+    /// Check if the server is running
+    [[nodiscard]] auto IsRunning() const -> bool { return running_.load(); }
+
+    /// Get the server configuration
+    [[nodiscard]] auto GetConfig() const -> const HttpServerConfig& { return config_; }
+
+    /// Broadcast a message to all connected WebSocket clients
+    void BroadcastWebSocket(const std::string& message);
+
+    /// Set the WebSocket message handler
+    void SetWebSocketMessageHandler(WebSocketMessageHandler handler);
+
+    /// Get the number of connected WebSocket clients
+    [[nodiscard]] auto GetWebSocketClientCount() const -> size_t;
+
+private:
+    void DoAccept();
+    void OnAccept(beast::error_code ec, tcp::socket socket);
+    void OnWebSocketConnect(const std::shared_ptr<WebSocketSession>& session);
+    void OnWebSocketDisconnect(const std::shared_ptr<WebSocketSession>& session);
+
+    HttpServerConfig config_;
+    std::atomic<bool> running_{false};
+
+    asio::io_context ioc_;
+    tcp::acceptor acceptor_;
+    std::vector<std::thread> threads_;
+
+    // WebSocket session management
+    std::unordered_set<std::shared_ptr<WebSocketSession>> ws_sessions_;
+    mutable std::mutex ws_sessions_mutex_;
+    WebSocketMessageHandler ws_message_handler_;
+};
+
+}  // namespace smartbotic::webserver

+ 463 - 0
webserver/src/config.cpp

@@ -0,0 +1,463 @@
+#include "smartbotic/webserver/config.hpp"
+
+#include <spdlog/spdlog.h>
+#include <yaml-cpp/yaml.h>
+
+#include <algorithm>
+#include <cctype>
+#include <cstdlib>
+#include <filesystem>
+#include <fstream>
+
+namespace smartbotic::webserver {
+
+auto LogLevelToString(LogLevel level) -> std::string {
+    switch (level) {
+        case LogLevel::kTrace:
+            return "trace";
+        case LogLevel::kDebug:
+            return "debug";
+        case LogLevel::kInfo:
+            return "info";
+        case LogLevel::kWarn:
+            return "warn";
+        case LogLevel::kError:
+            return "error";
+        case LogLevel::kCritical:
+            return "critical";
+        case LogLevel::kOff:
+            return "off";
+    }
+    return "info";
+}
+
+auto ParseLogLevel(const std::string& str) -> std::optional<LogLevel> {
+    std::string lower;
+    lower.reserve(str.size());
+    std::transform(str.begin(), str.end(), std::back_inserter(lower), [](unsigned char c) { return std::tolower(c); });
+
+    if (lower == "trace")
+        return LogLevel::kTrace;
+    if (lower == "debug")
+        return LogLevel::kDebug;
+    if (lower == "info")
+        return LogLevel::kInfo;
+    if (lower == "warn" || lower == "warning")
+        return LogLevel::kWarn;
+    if (lower == "error" || lower == "err")
+        return LogLevel::kError;
+    if (lower == "critical" || lower == "crit" || lower == "fatal")
+        return LogLevel::kCritical;
+    if (lower == "off" || lower == "none")
+        return LogLevel::kOff;
+
+    return std::nullopt;
+}
+
+namespace {
+
+auto GetEnvOrDefault(const char* name, const std::string& default_value) -> std::string {
+    const char* value = std::getenv(name);
+    return value != nullptr ? std::string(value) : default_value;
+}
+
+auto GetEnvOrDefault(const char* name, uint16_t default_value) -> uint16_t {
+    const char* value = std::getenv(name);
+    if (value == nullptr) {
+        return default_value;
+    }
+    try {
+        return static_cast<uint16_t>(std::stoi(value));
+    } catch (...) {
+        return default_value;
+    }
+}
+
+auto GetEnvBool(const char* name, bool default_value) -> bool {
+    const char* value = std::getenv(name);
+    if (value == nullptr) {
+        return default_value;
+    }
+    std::string str(value);
+    std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { return std::tolower(c); });
+    return str == "true" || str == "1" || str == "yes";
+}
+
+}  // namespace
+
+auto ConfigLoader::GetDefaultConfigPaths() -> std::vector<std::string> {
+    std::vector<std::string> paths;
+
+    // Current directory
+    paths.emplace_back("./smartbotic-server.yaml");
+    paths.emplace_back("./smartbotic-server.yml");
+
+    // Config subdirectory
+    paths.emplace_back("./config/smartbotic-server.yaml");
+    paths.emplace_back("./config/smartbotic-server.yml");
+
+    // /etc for system-wide config
+    paths.emplace_back("/etc/smartbotic/smartbotic-server.yaml");
+    paths.emplace_back("/etc/smartbotic/smartbotic-server.yml");
+
+    // Home directory
+    const char* home = std::getenv("HOME");
+    if (home != nullptr) {
+        paths.push_back(std::string(home) + "/.config/smartbotic/smartbotic-server.yaml");
+        paths.push_back(std::string(home) + "/.config/smartbotic/smartbotic-server.yml");
+    }
+
+    return paths;
+}
+
+auto ConfigLoader::FindConfigFile() -> std::optional<std::string> {
+    // Check environment variable for explicit config path
+    const char* config_path_env = std::getenv("SMARTBOTIC_SERVER_CONFIG");
+    if (config_path_env != nullptr && std::filesystem::exists(config_path_env)) {
+        return std::string(config_path_env);
+    }
+
+    // Search default paths
+    for (const auto& path : GetDefaultConfigPaths()) {
+        if (std::filesystem::exists(path)) {
+            return path;
+        }
+    }
+
+    return std::nullopt;
+}
+
+auto ConfigLoader::LoadFromFile(const std::string& path) -> std::optional<HttpServerConfig> {
+    try {
+        if (!std::filesystem::exists(path)) {
+            spdlog::warn("Config file not found: {}", path);
+            return std::nullopt;
+        }
+
+        YAML::Node yaml = YAML::LoadFile(path);
+        HttpServerConfig config;
+
+        // Server section
+        if (yaml["server"]) {
+            const auto& server = yaml["server"];
+            if (server["address"]) {
+                config.address = server["address"].as<std::string>();
+            }
+            if (server["port"]) {
+                config.port = server["port"].as<uint16_t>();
+            }
+            if (server["webui_path"]) {
+                config.webui_path = server["webui_path"].as<std::string>();
+            }
+        }
+
+        // WebSocket section
+        if (yaml["websocket"]) {
+            const auto& ws = yaml["websocket"];
+            if (ws["path"]) {
+                config.ws_path = ws["path"].as<std::string>();
+            }
+        }
+
+        // CORS section
+        if (yaml["cors"]) {
+            const auto& cors = yaml["cors"];
+            if (cors["enabled"]) {
+                config.cors.enabled = cors["enabled"].as<bool>();
+            }
+            if (cors["allowed_origins"]) {
+                config.cors.allowed_origins.clear();
+                for (const auto& origin : cors["allowed_origins"]) {
+                    config.cors.allowed_origins.push_back(origin.as<std::string>());
+                }
+            }
+            if (cors["allowed_methods"]) {
+                config.cors.allowed_methods.clear();
+                for (const auto& method : cors["allowed_methods"]) {
+                    config.cors.allowed_methods.push_back(method.as<std::string>());
+                }
+            }
+            if (cors["allowed_headers"]) {
+                config.cors.allowed_headers.clear();
+                for (const auto& header : cors["allowed_headers"]) {
+                    config.cors.allowed_headers.push_back(header.as<std::string>());
+                }
+            }
+            if (cors["allow_credentials"]) {
+                config.cors.allow_credentials = cors["allow_credentials"].as<bool>();
+            }
+            if (cors["max_age"]) {
+                config.cors.max_age = cors["max_age"].as<int>();
+            }
+        }
+
+        // Logging section
+        if (yaml["logging"]) {
+            const auto& logging = yaml["logging"];
+            if (logging["level"]) {
+                auto level_str = logging["level"].as<std::string>();
+                auto level = ParseLogLevel(level_str);
+                if (level) {
+                    config.log_level = *level;
+                } else {
+                    spdlog::warn("Invalid log level '{}', using default", level_str);
+                }
+            }
+        }
+
+        // Database section
+        if (yaml["database"]) {
+            const auto& database = yaml["database"];
+            if (database["address"]) {
+                config.database_address = database["address"].as<std::string>();
+            }
+        }
+
+        spdlog::info("Loaded configuration from: {}", path);
+        return config;
+
+    } catch (const YAML::Exception& e) {
+        spdlog::error("Failed to parse config file {}: {}", path, e.what());
+        return std::nullopt;
+    } catch (const std::exception& e) {
+        spdlog::error("Error loading config file {}: {}", path, e.what());
+        return std::nullopt;
+    }
+}
+
+void ConfigLoader::ApplyEnvironment(HttpServerConfig& config) {
+    // Server settings from environment
+    config.address = GetEnvOrDefault("SMARTBOTIC_SERVER_ADDRESS", config.address);
+    config.port = GetEnvOrDefault("SMARTBOTIC_SERVER_PORT", config.port);
+    config.webui_path = GetEnvOrDefault("SMARTBOTIC_SERVER_WEBUI_PATH", config.webui_path);
+    config.ws_path = GetEnvOrDefault("SMARTBOTIC_SERVER_WS_PATH", config.ws_path);
+
+    // CORS settings from environment
+    config.cors.enabled = GetEnvBool("SMARTBOTIC_SERVER_CORS_ENABLED", config.cors.enabled);
+
+    // Database settings from environment
+    config.database_address = GetEnvOrDefault("SMARTBOTIC_SERVER_DATABASE_ADDRESS", config.database_address);
+
+    // Log level from environment
+    const char* log_level_env = std::getenv("SMARTBOTIC_SERVER_LOG_LEVEL");
+    if (log_level_env != nullptr) {
+        auto level = ParseLogLevel(log_level_env);
+        if (level) {
+            config.log_level = *level;
+        }
+    }
+}
+
+auto ConfigLoader::ParseCommandLine(int argc, char* argv[], HttpServerConfig& config) -> std::optional<bool> {
+    std::string config_file_override;
+
+    for (int i = 1; i < argc; ++i) {
+        std::string arg = argv[i];
+
+        if (arg == "--help" || arg == "-h") {
+            PrintHelp(argv[0]);
+            return std::nullopt;
+        }
+
+        if (arg == "--version" || arg == "-v") {
+            spdlog::info("SmartBotic Web Server v0.1.0");
+            return std::nullopt;
+        }
+
+        if (arg == "--config" || arg == "-c") {
+            if (i + 1 < argc) {
+                config_file_override = argv[++i];
+            } else {
+                spdlog::error("--config requires a path argument");
+                return false;
+            }
+            continue;
+        }
+
+        if (arg == "--port" || arg == "-p") {
+            if (i + 1 < argc) {
+                try {
+                    config.port = static_cast<uint16_t>(std::stoi(argv[++i]));
+                } catch (...) {
+                    spdlog::error("Invalid port number: {}", argv[i]);
+                    return false;
+                }
+            } else {
+                spdlog::error("--port requires a number argument");
+                return false;
+            }
+            continue;
+        }
+
+        if (arg == "--address" || arg == "-a") {
+            if (i + 1 < argc) {
+                config.address = argv[++i];
+            } else {
+                spdlog::error("--address requires an address argument");
+                return false;
+            }
+            continue;
+        }
+
+        if (arg == "--webui-path" || arg == "-w") {
+            if (i + 1 < argc) {
+                config.webui_path = argv[++i];
+            } else {
+                spdlog::error("--webui-path requires a path argument");
+                return false;
+            }
+            continue;
+        }
+
+        if (arg == "--database" || arg == "-d") {
+            if (i + 1 < argc) {
+                config.database_address = argv[++i];
+            } else {
+                spdlog::error("--database requires an address argument");
+                return false;
+            }
+            continue;
+        }
+
+        if (arg == "--log-level" || arg == "-l") {
+            if (i + 1 < argc) {
+                auto level = ParseLogLevel(argv[++i]);
+                if (level) {
+                    config.log_level = *level;
+                } else {
+                    spdlog::error("Invalid log level: {}", argv[i]);
+                    return false;
+                }
+            } else {
+                spdlog::error("--log-level requires a level argument");
+                return false;
+            }
+            continue;
+        }
+
+        if (arg == "--no-cors") {
+            config.cors.enabled = false;
+            continue;
+        }
+
+        spdlog::error("Unknown argument: {}", arg);
+        PrintHelp(argv[0]);
+        return false;
+    }
+
+    // If config file was specified on command line, load it
+    if (!config_file_override.empty()) {
+        auto file_config = LoadFromFile(config_file_override);
+        if (!file_config) {
+            spdlog::error("Failed to load specified config file: {}", config_file_override);
+            return false;
+        }
+        config = *file_config;
+
+        // Re-parse command line to override file settings
+        for (int i = 1; i < argc; ++i) {
+            std::string arg = argv[i];
+            if (arg == "--config" || arg == "-c") {
+                ++i;
+                continue;
+            }
+            if ((arg == "--port" || arg == "-p") && i + 1 < argc) {
+                config.port = static_cast<uint16_t>(std::stoi(argv[++i]));
+            } else if ((arg == "--address" || arg == "-a") && i + 1 < argc) {
+                config.address = argv[++i];
+            } else if ((arg == "--webui-path" || arg == "-w") && i + 1 < argc) {
+                config.webui_path = argv[++i];
+            } else if ((arg == "--database" || arg == "-d") && i + 1 < argc) {
+                config.database_address = argv[++i];
+            } else if ((arg == "--log-level" || arg == "-l") && i + 1 < argc) {
+                auto level = ParseLogLevel(argv[++i]);
+                if (level)
+                    config.log_level = *level;
+            } else if (arg == "--no-cors") {
+                config.cors.enabled = false;
+            }
+        }
+    }
+
+    return true;
+}
+
+auto ConfigLoader::Load(int argc, char* argv[]) -> HttpServerConfig {
+    HttpServerConfig config = HttpServerConfig::Default();
+
+    // 1. Try to find and load config file
+    auto config_file = FindConfigFile();
+    if (config_file) {
+        auto file_config = LoadFromFile(*config_file);
+        if (file_config) {
+            config = *file_config;
+        }
+    }
+
+    // 2. Apply environment variables
+    ApplyEnvironment(config);
+
+    // 3. Parse command-line arguments
+    auto result = ParseCommandLine(argc, argv, config);
+    if (!result.has_value()) {
+        std::exit(0);
+    }
+    if (!result.value()) {
+        std::exit(1);
+    }
+
+    return config;
+}
+
+void ConfigLoader::PrintHelp(const char* program_name) {
+    spdlog::info("Usage: {} [options]", program_name);
+    spdlog::info("");
+    spdlog::info("SmartBotic Web Server - HTTP/WebSocket Server");
+    spdlog::info("");
+    spdlog::info("Options:");
+    spdlog::info("  -h, --help                 Show this help message");
+    spdlog::info("  -v, --version              Show version information");
+    spdlog::info("  -c, --config <path>        Path to config file (default: smartbotic-server.yaml)");
+    spdlog::info("  -p, --port <port>          Port to listen on (default: 8080)");
+    spdlog::info("  -a, --address <addr>       Address to bind to (default: 0.0.0.0)");
+    spdlog::info("  -w, --webui-path <path>    Path to WebUI static files (default: ./webui/dist)");
+    spdlog::info("  -d, --database <addr>      Database gRPC address (default: 127.0.0.1:50051)");
+    spdlog::info("  -l, --log-level <level>    Log level: trace, debug, info, warn, error, critical, off");
+    spdlog::info("  --no-cors                  Disable CORS headers");
+    spdlog::info("");
+    spdlog::info("Environment Variables:");
+    spdlog::info("  SMARTBOTIC_SERVER_CONFIG           Path to config file");
+    spdlog::info("  SMARTBOTIC_SERVER_ADDRESS          Address to bind to");
+    spdlog::info("  SMARTBOTIC_SERVER_PORT             Port to listen on");
+    spdlog::info("  SMARTBOTIC_SERVER_WEBUI_PATH       Path to WebUI static files");
+    spdlog::info("  SMARTBOTIC_SERVER_DATABASE_ADDRESS Database gRPC address");
+    spdlog::info("  SMARTBOTIC_SERVER_LOG_LEVEL        Log level");
+    spdlog::info("  SMARTBOTIC_SERVER_CORS_ENABLED     Enable/disable CORS (true/false)");
+    spdlog::info("");
+    spdlog::info("Config File Search Order:");
+    for (const auto& path : GetDefaultConfigPaths()) {
+        spdlog::info("  {}", path);
+    }
+}
+
+void ConfigLoader::PrintConfig(const HttpServerConfig& config) {
+    spdlog::info("Configuration:");
+    spdlog::info("  Address:          {}", config.address);
+    spdlog::info("  Port:             {}", config.port);
+    spdlog::info("  WebUI Path:       {}", config.webui_path);
+    spdlog::info("  WebSocket Path:   {}", config.ws_path);
+    spdlog::info("  Database Address: {}", config.database_address);
+    spdlog::info("  Log Level:        {}", LogLevelToString(config.log_level));
+    spdlog::info("  CORS Enabled:     {}", config.cors.enabled ? "yes" : "no");
+    if (config.cors.enabled) {
+        std::string origins;
+        for (size_t i = 0; i < config.cors.allowed_origins.size(); ++i) {
+            if (i > 0)
+                origins += ", ";
+            origins += config.cors.allowed_origins[i];
+        }
+        spdlog::info("  CORS Origins:     {}", origins);
+    }
+}
+
+}  // namespace smartbotic::webserver

+ 616 - 0
webserver/src/http_server.cpp

@@ -0,0 +1,616 @@
+#include "smartbotic/webserver/http_server.hpp"
+
+#include <spdlog/spdlog.h>
+
+#include <chrono>
+#include <filesystem>
+#include <fstream>
+#include <tuple>
+#include <utility>
+
+namespace smartbotic::webserver {
+
+// ============================================================================
+// WebSocketSession Implementation
+// ============================================================================
+
+WebSocketSession::WebSocketSession(tcp::socket socket, const HttpServerConfig& config)
+    : ws_(std::move(socket)), config_(config) {}
+
+WebSocketSession::~WebSocketSession() {
+    spdlog::debug("WebSocket session destroyed");
+}
+
+void WebSocketSession::Run(http::request<http::string_body> req) {
+    DoAccept(std::move(req));
+}
+
+void WebSocketSession::Send(const std::string& message) {
+    auto msg = std::make_shared<std::string const>(message);
+
+    std::lock_guard<std::mutex> lock(mutex_);
+    queue_.push_back(msg);
+
+    if (queue_.size() > 1) {
+        return;
+    }
+
+    ws_.async_write(asio::buffer(*queue_.front()),
+                    beast::bind_front_handler(&WebSocketSession::OnWrite, shared_from_this()));
+}
+
+void WebSocketSession::Close() {
+    beast::error_code ec;
+    ws_.close(websocket::close_code::normal, ec);
+}
+
+auto WebSocketSession::IsOpen() const -> bool {
+    return ws_.is_open();
+}
+
+void WebSocketSession::SetMessageHandler(WebSocketMessageHandler handler) {
+    message_handler_ = std::move(handler);
+}
+
+void WebSocketSession::DoAccept(http::request<http::string_body> req) {  // NOLINT(performance-unnecessary-value-param)
+    ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::server));
+
+    ws_.set_option(websocket::stream_base::decorator(
+        [](websocket::response_type& res) { res.set(http::field::server, "SmartBotic-Server/0.1.0"); }));
+
+    ws_.async_accept(req, beast::bind_front_handler(&WebSocketSession::OnAccept, shared_from_this()));
+}
+
+void WebSocketSession::OnAccept(beast::error_code ec) {
+    if (ec) {
+        spdlog::error("WebSocket accept error: {}", ec.message());
+        return;
+    }
+
+    spdlog::info("WebSocket connection established");
+    DoRead();
+}
+
+void WebSocketSession::DoRead() {
+    ws_.async_read(buffer_, beast::bind_front_handler(&WebSocketSession::OnRead, shared_from_this()));
+}
+
+void WebSocketSession::OnRead(beast::error_code ec, std::size_t bytes_transferred) {
+    if (ec == websocket::error::closed) {
+        spdlog::info("WebSocket connection closed by client");
+        return;
+    }
+
+    if (ec) {
+        spdlog::error("WebSocket read error: {}", ec.message());
+        return;
+    }
+
+    std::string message = beast::buffers_to_string(buffer_.data());
+    buffer_.consume(buffer_.size());
+
+    spdlog::debug("WebSocket received: {} bytes", bytes_transferred);
+
+    if (message_handler_) {
+        message_handler_(*this, message);
+    }
+
+    DoRead();
+}
+
+void WebSocketSession::OnWrite(beast::error_code ec, std::size_t bytes_transferred) {
+    if (ec) {
+        spdlog::error("WebSocket write error: {}", ec.message());
+        return;
+    }
+
+    spdlog::debug("WebSocket sent: {} bytes", bytes_transferred);
+
+    std::lock_guard<std::mutex> lock(mutex_);
+    queue_.erase(queue_.begin());
+
+    if (!queue_.empty()) {
+        ws_.async_write(asio::buffer(*queue_.front()),
+                        beast::bind_front_handler(&WebSocketSession::OnWrite, shared_from_this()));
+    }
+}
+
+// ============================================================================
+// HttpSession Implementation
+// ============================================================================
+
+HttpSession::HttpSession(tcp::socket socket, const HttpServerConfig& config,
+                         std::function<void(std::shared_ptr<WebSocketSession>)> ws_connect_handler,
+                         std::function<void(std::shared_ptr<WebSocketSession>)> ws_disconnect_handler)
+    : stream_(std::move(socket)),
+      config_(config),
+      ws_connect_handler_(std::move(ws_connect_handler)),
+      ws_disconnect_handler_(std::move(ws_disconnect_handler)) {}
+
+HttpSession::~HttpSession() {
+    spdlog::debug("HTTP session destroyed");
+}
+
+void HttpSession::Run() {
+    asio::dispatch(stream_.get_executor(), beast::bind_front_handler(&HttpSession::DoRead, shared_from_this()));
+}
+
+void HttpSession::DoRead() {
+    req_ = {};
+    stream_.expires_after(std::chrono::seconds(30));
+    http::async_read(stream_, buffer_, req_, beast::bind_front_handler(&HttpSession::OnRead, shared_from_this()));
+}
+
+void HttpSession::OnRead(beast::error_code ec, std::size_t bytes_transferred) {
+    std::ignore = bytes_transferred;
+
+    if (ec == http::error::end_of_stream) {
+        std::ignore = stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
+        return;
+    }
+
+    if (ec) {
+        spdlog::error("HTTP read error: {}", ec.message());
+        return;
+    }
+
+    // Capture request info for logging
+    request_start_ = std::chrono::steady_clock::now();
+    request_method_ = std::string(req_.method_string());
+    request_target_ = std::string(req_.target());
+
+    HandleRequest(std::move(req_));
+}
+
+void HttpSession::HandleRequest(http::request<http::string_body>&& req) {
+    std::string target(req.target());
+
+    // Handle CORS preflight
+    if (req.method() == http::verb::options && config_.cors.enabled) {
+        auto res = http::response<http::string_body>{http::status::no_content, req.version()};
+        res.set(http::field::server, "SmartBotic-Server/0.1.0");
+        ApplyCorsHeaders(res);
+        res.prepare_payload();
+        SendResponse(std::move(res));
+        return;
+    }
+
+    // Check for WebSocket upgrade
+    if (websocket::is_upgrade(req) && target == config_.ws_path) {
+        spdlog::info("WebSocket upgrade request for {}", target);
+
+        auto ws_session = std::make_shared<WebSocketSession>(stream_.release_socket(), config_);
+
+        if (ws_connect_handler_) {
+            ws_connect_handler_(ws_session);
+        }
+
+        ws_session->Run(std::move(req));
+        return;
+    }
+
+    http::response<http::string_body> res;
+
+    // Route API requests
+    if (target.starts_with("/api/")) {
+        res = HandleApiRequest(req);
+    }
+    // Serve static files for WebUI
+    else {
+        res = HandleStaticFile(target);
+    }
+
+    res.version(req.version());
+    res.set(http::field::server, "SmartBotic-Server/0.1.0");
+
+    if (config_.cors.enabled) {
+        ApplyCorsHeaders(res);
+    }
+
+    res.prepare_payload();
+    SendResponse(std::move(res));
+}
+
+auto HttpSession::HandleStaticFile(const std::string& target) -> http::response<http::string_body> {
+    std::string path = target;
+
+    // Default to index.html for root
+    if (path == "/" || path.empty()) {
+        path = "/index.html";
+    }
+
+    // Security: prevent directory traversal
+    if (path.find("..") != std::string::npos) {
+        return MakeErrorResponse(http::status::forbidden, "Invalid path");
+    }
+
+    std::filesystem::path file_path = config_.webui_path + path;
+
+    // If path is a directory, try index.html
+    if (std::filesystem::is_directory(file_path)) {
+        file_path /= "index.html";
+    }
+
+    if (!std::filesystem::exists(file_path)) {
+        // For SPA routing, return index.html for non-file paths
+        std::filesystem::path index_path = config_.webui_path + "/index.html";
+        if (std::filesystem::exists(index_path)) {
+            file_path = index_path;
+        } else {
+            return MakeErrorResponse(http::status::not_found, "File not found");
+        }
+    }
+
+    // Read file content
+    std::ifstream file(file_path, std::ios::binary);
+    if (!file) {
+        return MakeErrorResponse(http::status::internal_server_error, "Failed to read file");
+    }
+
+    std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
+
+    http::response<http::string_body> res{http::status::ok, 11};
+    res.set(http::field::content_type, GetMimeType(file_path.string()));
+    res.body() = std::move(content);
+
+    return res;
+}
+
+auto HttpSession::HandleApiRequest(http::request<http::string_body>& req) -> http::response<http::string_body> {
+    std::string target(req.target());
+
+    // Health check endpoint
+    if (target == "/api/health" && req.method() == http::verb::get) {
+        http::response<http::string_body> res{http::status::ok, req.version()};
+        res.set(http::field::content_type, "application/json");
+        res.body() = R"({"status":"healthy","service":"smartbotic-server"})";
+        return res;
+    }
+
+    // Version endpoint
+    if (target == "/api/version" && req.method() == http::verb::get) {
+        http::response<http::string_body> res{http::status::ok, req.version()};
+        res.set(http::field::content_type, "application/json");
+        res.body() = R"({"version":"0.1.0","name":"SmartBotic Web Server"})";
+        return res;
+    }
+
+    // Default: not found
+    return MakeErrorResponse(http::status::not_found, "API endpoint not found");
+}
+
+auto HttpSession::MakeErrorResponse(
+    http::status status, const std::string& message)  // NOLINT(readability-convert-member-functions-to-static)
+    -> http::response<http::string_body> {
+    http::response<http::string_body> res{status, 11};
+    res.set(http::field::content_type, "application/json");
+    res.body() = R"({"error":")" + message + R"("})";
+    return res;
+}
+
+void HttpSession::ApplyCorsHeaders(http::response<http::string_body>& res) {
+    if (!config_.cors.enabled) {
+        return;
+    }
+
+    // Combine allowed origins
+    std::string origins;
+    for (size_t i = 0; i < config_.cors.allowed_origins.size(); ++i) {
+        if (i > 0) {
+            origins += ", ";
+        }
+        origins += config_.cors.allowed_origins[i];
+    }
+    res.set(http::field::access_control_allow_origin, origins);
+
+    // Combine allowed methods
+    std::string methods;
+    for (size_t i = 0; i < config_.cors.allowed_methods.size(); ++i) {
+        if (i > 0) {
+            methods += ", ";
+        }
+        methods += config_.cors.allowed_methods[i];
+    }
+    res.set(http::field::access_control_allow_methods, methods);
+
+    // Combine allowed headers
+    std::string headers;
+    for (size_t i = 0; i < config_.cors.allowed_headers.size(); ++i) {
+        if (i > 0) {
+            headers += ", ";
+        }
+        headers += config_.cors.allowed_headers[i];
+    }
+    res.set(http::field::access_control_allow_headers, headers);
+
+    if (config_.cors.allow_credentials) {
+        res.set(http::field::access_control_allow_credentials, "true");
+    }
+
+    res.set(http::field::access_control_max_age, std::to_string(config_.cors.max_age));
+}
+
+auto HttpSession::GetMimeType(const std::string& path)
+    -> std::string {  // NOLINT(readability-convert-member-functions-to-static)
+    auto ext_pos = path.rfind('.');
+    if (ext_pos == std::string::npos) {
+        return "application/octet-stream";
+    }
+
+    std::string ext = path.substr(ext_pos);
+
+    if (ext == ".html" || ext == ".htm") {
+        return "text/html";
+    }
+    if (ext == ".css") {
+        return "text/css";
+    }
+    if (ext == ".js") {
+        return "application/javascript";
+    }
+    if (ext == ".json") {
+        return "application/json";
+    }
+    if (ext == ".png") {
+        return "image/png";
+    }
+    if (ext == ".jpg" || ext == ".jpeg") {
+        return "image/jpeg";
+    }
+    if (ext == ".gif") {
+        return "image/gif";
+    }
+    if (ext == ".svg") {
+        return "image/svg+xml";
+    }
+    if (ext == ".ico") {
+        return "image/x-icon";
+    }
+    if (ext == ".woff") {
+        return "font/woff";
+    }
+    if (ext == ".woff2") {
+        return "font/woff2";
+    }
+    if (ext == ".ttf") {
+        return "font/ttf";
+    }
+    if (ext == ".eot") {
+        return "application/vnd.ms-fontobject";
+    }
+    if (ext == ".txt") {
+        return "text/plain";
+    }
+    if (ext == ".xml") {
+        return "application/xml";
+    }
+    if (ext == ".pdf") {
+        return "application/pdf";
+    }
+
+    return "application/octet-stream";
+}
+
+void HttpSession::SendResponse(http::response<http::string_body>&& res) {
+    auto sp = std::make_shared<http::response<http::string_body>>(std::move(res));
+    auto status = sp->result();
+    auto self = shared_from_this();
+
+    http::async_write(stream_, *sp, [self, sp, status](beast::error_code ec, std::size_t bytes_transferred) {
+        self->LogRequest(status);
+        self->OnWrite(ec, bytes_transferred, sp->need_eof());
+    });
+}
+
+void HttpSession::LogRequest(http::status status) {
+    auto now = std::chrono::steady_clock::now();
+    auto duration_us = std::chrono::duration_cast<std::chrono::microseconds>(now - request_start_).count();
+
+    // Format duration as ms with microsecond precision
+    double duration_ms = static_cast<double>(duration_us) / 1000.0;
+
+    auto status_int = static_cast<unsigned>(status);
+
+    // Log at appropriate level based on status code
+    if (status_int >= 500) {
+        spdlog::error("{} {} {} {:.2f}ms", request_method_, request_target_, status_int, duration_ms);
+    } else if (status_int >= 400) {
+        spdlog::warn("{} {} {} {:.2f}ms", request_method_, request_target_, status_int, duration_ms);
+    } else {
+        spdlog::info("{} {} {} {:.2f}ms", request_method_, request_target_, status_int, duration_ms);
+    }
+}
+
+void HttpSession::OnWrite(beast::error_code ec, std::size_t bytes_transferred, bool close) {
+    std::ignore = bytes_transferred;
+
+    if (ec) {
+        spdlog::error("HTTP write error: {}", ec.message());
+        return;
+    }
+
+    if (close) {
+        beast::error_code ec_close;
+        std::ignore = stream_.socket().shutdown(tcp::socket::shutdown_send, ec_close);
+        return;
+    }
+
+    DoRead();
+}
+
+// ============================================================================
+// HttpServer Implementation
+// ============================================================================
+
+HttpServer::HttpServer(HttpServerConfig config) : config_(std::move(config)), ioc_(1), acceptor_(ioc_) {}
+
+HttpServer::~HttpServer() {
+    if (running_.load()) {
+        Stop();
+    }
+}
+
+auto HttpServer::Start() -> bool {
+    if (running_.load()) {
+        spdlog::warn("Server already running");
+        return true;
+    }
+
+    spdlog::info("Starting HTTP server on {}", config_.GetListenAddress());
+
+    try {
+        auto const kAddress = asio::ip::make_address(config_.address);
+        tcp::endpoint endpoint{kAddress, config_.port};
+
+        beast::error_code ec;
+
+        acceptor_.open(endpoint.protocol(), ec);
+        if (ec) {
+            spdlog::error("Failed to open acceptor: {}", ec.message());
+            return false;
+        }
+
+        acceptor_.set_option(asio::socket_base::reuse_address(true), ec);
+        if (ec) {
+            spdlog::error("Failed to set reuse_address: {}", ec.message());
+            return false;
+        }
+
+        acceptor_.bind(endpoint, ec);
+        if (ec) {
+            spdlog::error("Failed to bind to {}: {}", config_.GetListenAddress(), ec.message());
+            return false;
+        }
+
+        acceptor_.listen(asio::socket_base::max_listen_connections, ec);
+        if (ec) {
+            spdlog::error("Failed to listen: {}", ec.message());
+            return false;
+        }
+
+        running_.store(true);
+        DoAccept();
+
+        // Start the I/O context in a thread
+        threads_.emplace_back([this]() { ioc_.run(); });
+
+        spdlog::info("HTTP server started successfully on {}", config_.GetListenAddress());
+        spdlog::info("WebSocket endpoint: ws://{}{}", config_.GetListenAddress(), config_.ws_path);
+        spdlog::info("Static files: {}", config_.webui_path);
+
+        return true;
+    } catch (const std::exception& e) {
+        spdlog::error("Failed to start HTTP server: {}", e.what());
+        return false;
+    }
+}
+
+void HttpServer::Stop() {
+    if (!running_.load()) {
+        return;
+    }
+
+    spdlog::info("Stopping HTTP server...");
+
+    running_.store(false);
+
+    // Close all WebSocket sessions
+    {
+        std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
+        for (const auto& session : ws_sessions_) {
+            if (session->IsOpen()) {
+                session->Close();
+            }
+        }
+        ws_sessions_.clear();
+    }
+
+    // Stop accepting new connections
+    beast::error_code ec;
+    std::ignore = acceptor_.close(ec);
+
+    // Stop the I/O context
+    ioc_.stop();
+
+    // Wait for threads to finish
+    for (auto& thread : threads_) {
+        if (thread.joinable()) {
+            thread.join();
+        }
+    }
+    threads_.clear();
+
+    spdlog::info("HTTP server stopped");
+}
+
+void HttpServer::Wait() {
+    for (auto& thread : threads_) {
+        if (thread.joinable()) {
+            thread.join();
+        }
+    }
+}
+
+void HttpServer::BroadcastWebSocket(const std::string& message) {
+    std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
+    for (const auto& session : ws_sessions_) {
+        if (session->IsOpen()) {
+            session->Send(message);
+        }
+    }
+}
+
+void HttpServer::SetWebSocketMessageHandler(WebSocketMessageHandler handler) {
+    ws_message_handler_ = std::move(handler);
+}
+
+auto HttpServer::GetWebSocketClientCount() const -> size_t {
+    std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
+    return ws_sessions_.size();
+}
+
+void HttpServer::DoAccept() {
+    acceptor_.async_accept(ioc_, beast::bind_front_handler(&HttpServer::OnAccept, this));
+}
+
+void HttpServer::OnAccept(beast::error_code ec, tcp::socket socket) {
+    if (ec) {
+        if (running_.load()) {
+            spdlog::error("Accept error: {}", ec.message());
+        }
+        return;
+    }
+
+    auto endpoint = socket.remote_endpoint(ec);
+    if (!ec) {
+        spdlog::debug("New connection from {}:{}", endpoint.address().to_string(), endpoint.port());
+    }
+
+    auto session = std::make_shared<HttpSession>(
+        std::move(socket), config_, [this](std::shared_ptr<WebSocketSession> ws) { OnWebSocketConnect(std::move(ws)); },
+        [this](std::shared_ptr<WebSocketSession> ws) { OnWebSocketDisconnect(std::move(ws)); });
+
+    session->Run();
+
+    if (running_.load()) {
+        DoAccept();
+    }
+}
+
+void HttpServer::OnWebSocketConnect(const std::shared_ptr<WebSocketSession>& session) {
+    if (ws_message_handler_) {
+        session->SetMessageHandler(ws_message_handler_);
+    }
+
+    std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
+    ws_sessions_.insert(session);
+    spdlog::info("WebSocket client connected (total: {})", ws_sessions_.size());
+}
+
+void HttpServer::OnWebSocketDisconnect(const std::shared_ptr<WebSocketSession>& session) {
+    std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
+    ws_sessions_.erase(session);
+    spdlog::info("WebSocket client disconnected (total: {})", ws_sessions_.size());
+}
+
+}  // namespace smartbotic::webserver

+ 109 - 2
webserver/src/main.cpp

@@ -1,15 +1,122 @@
 #include <spdlog/spdlog.h>
 
+#include <atomic>
+#include <csignal>
+#include <cstdlib>
+#include <string>
+
 #include "smartbotic/common.hpp"
+#include "smartbotic/webserver/config.hpp"
+#include "smartbotic/webserver/http_server.hpp"
+
+namespace {
+
+// Global server pointer for signal handling
+smartbotic::webserver::HttpServer* g_server = nullptr;
+
+// Atomic flag for shutdown tracking
+std::atomic<bool> g_shutdown_requested{false};
+
+void SignalHandler(int signal) {
+    if (g_shutdown_requested.exchange(true)) {
+        // Second signal - force exit
+        spdlog::warn("Received second signal {}, forcing immediate exit", signal);
+        std::_Exit(1);
+    }
+
+    const char* signal_name = (signal == SIGINT) ? "SIGINT" : "SIGTERM";
+    spdlog::info("Received {} ({}), initiating graceful shutdown...", signal_name, signal);
+
+    if (g_server != nullptr) {
+        g_server->Stop();
+    }
+}
+
+auto SetLogLevel(smartbotic::webserver::LogLevel level) {
+    switch (level) {
+        case smartbotic::webserver::LogLevel::kTrace:
+            spdlog::set_level(spdlog::level::trace);
+            break;
+        case smartbotic::webserver::LogLevel::kDebug:
+            spdlog::set_level(spdlog::level::debug);
+            break;
+        case smartbotic::webserver::LogLevel::kInfo:
+            spdlog::set_level(spdlog::level::info);
+            break;
+        case smartbotic::webserver::LogLevel::kWarn:
+            spdlog::set_level(spdlog::level::warn);
+            break;
+        case smartbotic::webserver::LogLevel::kError:
+            spdlog::set_level(spdlog::level::err);
+            break;
+        case smartbotic::webserver::LogLevel::kCritical:
+            spdlog::set_level(spdlog::level::critical);
+            break;
+        case smartbotic::webserver::LogLevel::kOff:
+            spdlog::set_level(spdlog::level::off);
+            break;
+    }
+}
+
+}  // namespace
 
 int main(int argc, char* argv[]) {
+    // Initialize common logging first (with default settings)
     smartbotic::common::Initialize("smartbotic-server");
 
     spdlog::info("SmartBotic Web Server starting...");
 
-    // TODO: Implement web server
+    // Load configuration from file, environment, and command line
+    auto config = smartbotic::webserver::ConfigLoader::Load(argc, argv);
+
+    // Apply configured log level
+    SetLogLevel(config.log_level);
+
+    // Print configuration
+    smartbotic::webserver::ConfigLoader::PrintConfig(config);
+
+    // Create and start the server
+    smartbotic::webserver::HttpServer server(config);
+    g_server = &server;
+
+    // Set up signal handlers for graceful shutdown (SIGTERM/SIGINT)
+    struct sigaction sa{};
+    sa.sa_handler = SignalHandler;
+    sigemptyset(&sa.sa_mask);
+    sa.sa_flags = 0;
+
+    if (sigaction(SIGINT, &sa, nullptr) == -1) {
+        spdlog::error("Failed to set SIGINT handler");
+        smartbotic::common::Shutdown();
+        return 1;
+    }
+    if (sigaction(SIGTERM, &sa, nullptr) == -1) {
+        spdlog::error("Failed to set SIGTERM handler");
+        smartbotic::common::Shutdown();
+        return 1;
+    }
+
+    spdlog::info("Signal handlers registered for SIGINT and SIGTERM");
+
+    // Set up WebSocket message handler for real-time updates
+    server.SetWebSocketMessageHandler([](smartbotic::webserver::WebSocketSession& session, const std::string& message) {
+        spdlog::debug("WebSocket message received: {}", message);
+        // Echo back for now - in a real implementation, this would process
+        // commands and send appropriate responses
+        session.Send(R"({"type":"ack","message":"received"})");
+    });
+
+    if (!server.Start()) {
+        spdlog::error("Failed to start server");
+        smartbotic::common::Shutdown();
+        return 1;
+    }
+
+    // Wait for server to stop (either by signal or error)
+    server.Wait();
 
-    spdlog::info("SmartBotic Web Server shutting down");
+    spdlog::info("SmartBotic Web Server shutdown complete");
+    g_server = nullptr;
     smartbotic::common::Shutdown();
 
     return 0;