(* REST API handlers *) open Lwt.Infix open Dream open Database open Monitor (* Utility functions *) let get_param_int64 req name = try let str = Dream.param req name in Some (Int64.of_string str) with _ -> None let get_param_int req name default = try Some (int_of_string (Dream.param req name)) with _ -> Some default let get_param_bool req name default = try Some (bool_of_string (Dream.param req name)) with _ -> Some default (* JSON response helpers *) let ok_response data = let json = Yojson.Basic.(`Assoc [("success", `Bool true); ("data", data)]) in Dream.json ~status:`OK json let error_response message = let json = Yojson.Basic.(`Assoc [("success", `Bool false); ("error", `String message)]) in Dream.json ~status:`Bad_Request json let not_found_response resource = let json = Yojson.Basic.( `Assoc [("success", `Bool false); ("error", `String (Printf.sprintf "%s not found" resource))]) in Dream.json ~status:`Not_Found json let internal_error_response message = let json = Yojson.Basic.(`Assoc [("success", `Bool false); ("error", `String message)]) in Dream.json ~status:`Internal_Server_Error json (* Website API handlers *) let list_websites req = Websites.get_all () >>= fun websites -> let websites_json = List.map (fun (w : Website.t) -> Yojson.Basic.( `Assoc [ ("id", `String (Int64.to_string w.id)); ("name", `String w.name); ("url", `String w.url); ("expected_status", `Int w.expected_status); ("timeout", `Int w.timeout); ("check_interval", `Int w.check_interval); ("active", `Bool w.active); ("created_at", `String (Ptime.to_rfc3339 w.created_at)); ("updated_at", `String (Ptime.to_rfc3339 w.updated_at)); ("last_checked", (match w.last_checked with | None -> `Null | Some t -> `String (Ptime.to_rfc3339 t))); ("last_status", (match w.last_status with | None -> `Null | Some s -> `Int s)); ])) websites in ok_response (`List websites_json) let create_website req = Dream.json req >>= fun json -> let open Yojson.Basic.Util in try let name = json |> member "name" |> to_string in let url = json |> member "url" |> to_string in let expected_status = (try json |> member "expected_status" |> to_int with _ -> 200) in let timeout = (try json |> member "timeout" |> to_int with _ -> 30) in let check_interval = (try json |> member "check_interval" |> to_int with _ -> 300) in Websites.create_website name url expected_status timeout check_interval () >>= fun () -> Websites.get_all () >>= fun websites -> (* Get the last created website *) let new_website = List.hd (List.rev websites) in let website_json = Yojson.Basic.( `Assoc [ ("id", `String (Int64.to_string new_website.id)); ("name", `String new_website.name); ("url", `String new_website.url); ("expected_status", `Int new_website.expected_status); ("timeout", `Int new_website.timeout); ("check_interval", `Int new_website.check_interval); ("active", `Bool new_website.active); ("created_at", `String (Ptime.to_rfc3339 new_website.created_at)); ]) in ok_response website_json with exn -> Logs.err (fun m -> m "Error creating website: %s" (Printexc.to_string exn)); error_response (Printexc.to_string exn) let get_website req = match get_param_int64 req "id" with | None -> error_response "Invalid website ID" | Some id -> Websites.get_by_id id >>= function | None -> not_found_response "Website" | Some website -> let website_json = Yojson.Basic.( `Assoc [ ("id", `String (Int64.to_string website.id)); ("name", `String website.name); ("url", `String website.url); ("expected_status", `Int website.expected_status); ("timeout", `Int website.timeout); ("check_interval", `Int website.check_interval); ("active", `Bool website.active); ("created_at", `String (Ptime.to_rfc3339 website.created_at)); ("updated_at", `String (Ptime.to_rfc3339 website.updated_at)); ("last_checked", (match website.last_checked with | None -> `Null | Some t -> `String (Ptime.to_rfc3339 t))); ("last_status", (match website.last_status with | None -> `Null | Some s -> `Int s)); ]) in ok_response website_json let update_website req = match get_param_int64 req "id" with | None -> error_response "Invalid website ID" | Some id -> Dream.json req >>= fun json -> let open Yojson.Basic.Util in try Websites.get_by_id id >>= function | None -> not_found_response "Website" | Some website -> let name = (try Some (json |> member "name" |> to_string) with _ -> Some website.name) in let url = (try Some (json |> member "url" |> to_string) with _ -> Some website.url) in let expected_status = get_param_int_from_json json "expected_status" website.expected_status in let timeout = get_param_int_from_json json "timeout" website.timeout in let check_interval = get_param_int_from_json json "check_interval" website.check_interval in let active = get_param_bool_from_json json "active" website.active in Websites.update_website id name url expected_status timeout check_interval active () >>= fun () -> Websites.get_by_id id >>= function | None -> internal_error_response "Failed to retrieve updated website" | Some updated -> let website_json = Yojson.Basic.( `Assoc [ ("id", `String (Int64.to_string updated.id)); ("name", `String updated.name); ("url", `String updated.url); ("expected_status", `Int updated.expected_status); ("timeout", `Int updated.timeout); ("check_interval", `Int updated.check_interval); ("active", `Bool updated.active); ("updated_at", `String (Ptime.to_rfc3339 updated.updated_at)); ]) in ok_response website_json with exn -> Logs.err (fun m -> m "Error updating website: %s" (Printexc.to_string exn)); error_response (Printexc.to_string exn) let delete_website req = match get_param_int64 req "id" with | None -> error_response "Invalid website ID" | Some id -> Websites.get_by_id id >>= function | None -> not_found_response "Website" | Some _ -> Websites.delete_website id () >>= fun () -> ok_response (`String "Website deleted successfully") let check_website_now req = match get_param_int64 req "id" with | None -> error_response "Invalid website ID" | Some id -> Websites.get_by_id id >>= function | None -> not_found_response "Website" | Some website -> check_and_store_website website >>= fun () -> ok_response (`String "Website check initiated") let get_website_history req = match get_param_int64 req "id" with | None -> error_response "Invalid website ID" | Some id -> let limit = match Dream.query req "limit" with | None -> 100 | Some l -> (try int_of_string l with _ -> 100) in CheckHistories.get_by_website_id id limit >>= fun histories -> let histories_json = List.map (fun (h : CheckHistory.t) -> Yojson.Basic.( `Assoc [ ("id", `String (Int64.to_string h.id)); ("status_code", `Int h.status_code); ("response_time", `Float h.response_time); ("error_message", (match h.error_message with | None -> `Null | Some msg -> `String msg)); ("checked_at", `String (Ptime.to_rfc3339 h.checked_at)); ])) histories in ok_response (`List histories_json) let get_website_status req = match get_param_int64 req "id" with | None -> error_response "Invalid website ID" | Some id -> Monitor.get_website_status id >>= fun status_json -> ok_response status_json (* Alert API handlers *) let list_alerts req = Alerts.get_all () >>= fun alerts -> let alerts_json = List.map (fun (a : Alert.t) -> Yojson.Basic.( `Assoc [ ("id", `String (Int64.to_string a.id)); ("website_id", `String (Int64.to_string a.website_id)); ("alert_type", `String a.alert_type); ("config", `String a.config); ("enabled", `Bool a.enabled); ("created_at", `String (Ptime.to_rfc3339 a.created_at)); ("updated_at", `String (Ptime.to_rfc3339 a.updated_at)); ])) alerts in ok_response (`List alerts_json) let create_alert req = Dream.json req >>= fun json -> let open Yojson.Basic.Util in try let website_id = Int64.of_string (json |> member "website_id" |> to_string) in let alert_type = json |> member "alert_type" |> to_string in let config = Yojson.Basic.to_string (json |> member "config") in Alerts.create_alert website_id alert_type config () >>= fun () -> Alerts.get_all () >>= fun alerts -> let new_alert = List.hd (List.rev alerts) in let alert_json = Yojson.Basic.( `Assoc [ ("id", `String (Int64.to_string new_alert.id)); ("website_id", `String (Int64.to_string new_alert.website_id)); ("alert_type", `String new_alert.alert_type); ("config", `String new_alert.config); ("enabled", `Bool new_alert.enabled); ("created_at", `String (Ptime.to_rfc3339 new_alert.created_at)); ]) in ok_response alert_json with exn -> Logs.err (fun m -> m "Error creating alert: %s" (Printexc.to_string exn)); error_response (Printexc.to_string exn) let get_alert req = match get_param_int64 req "id" with | None -> error_response "Invalid alert ID" | Some id -> Alerts.get_by_id id >>= function | None -> not_found_response "Alert" | Some alert -> let alert_json = Yojson.Basic.( `Assoc [ ("id", `String (Int64.to_string alert.id)); ("website_id", `String (Int64.to_string alert.website_id)); ("alert_type", `String alert.alert_type); ("config", `String alert.config); ("enabled", `Bool alert.enabled); ("created_at", `String (Ptime.to_rfc3339 alert.created_at)); ("updated_at", `String (Ptime.to_rfc3339 alert.updated_at)); ]) in ok_response alert_json let update_alert req = match get_param_int64 req "id" with | None -> error_response "Invalid alert ID" | Some id -> Dream.json req >>= fun json -> let open Yojson.Basic.Util in try Alerts.get_by_id id >>= function | None -> not_found_response "Alert" | Some alert -> let alert_type = (try Some (json |> member "alert_type" |> to_string) with _ -> Some alert.alert_type) in let config = (try Some (Yojson.Basic.to_string (json |> member "config")) with _ -> Some alert.config) in let enabled = get_param_bool_from_json json "enabled" alert.enabled in Alerts.update_alert id alert_type config enabled () >>= fun () -> Alerts.get_by_id id >>= function | None -> internal_error_response "Failed to retrieve updated alert" | Some updated -> let alert_json = Yojson.Basic.( `Assoc [ ("id", `String (Int64.to_string updated.id)); ("website_id", `String (Int64.to_string updated.website_id)); ("alert_type", `String updated.alert_type); ("config", `String updated.config); ("enabled", `Bool updated.enabled); ("updated_at", `String (Ptime.to_rfc3339 updated.updated_at)); ]) in ok_response alert_json with exn -> Logs.err (fun m -> m "Error updating alert: %s" (Printexc.to_string exn)); error_response (Printexc.to_string exn) let delete_alert req = match get_param_int64 req "id" with | None -> error_response "Invalid alert ID" | Some id -> Alerts.get_by_id id >>= function | None -> not_found_response "Alert" | Some _ -> Alerts.delete_alert id () >>= fun () -> ok_response (`String "Alert deleted successfully") (* Stats API handlers *) let get_stats_summary req = Websites.get_all () >>= fun websites -> let total = List.length websites in let active = List.fold_left (fun acc w -> if w.active then acc + 1 else acc) 0 websites in let healthy = List.fold_left (fun acc w -> match w.last_status with | None -> acc | Some status -> if status = w.expected_status then acc + 1 else acc) 0 websites in let stats_json = Yojson.Basic.( `Assoc [ ("total_websites", `Int total); ("active_websites", `Int active); ("healthy_websites", `Int healthy); ("unhealthy_websites", `Int (active - healthy)); ]) in ok_response stats_json (* Helper functions for parsing JSON parameters *) let get_param_int_from_json json name default = try Some (Yojson.Basic.Util.(json |> member name |> to_int)) with _ -> Some default let get_param_bool_from_json json name default = try Some (Yojson.Basic.Util.(json |> member name |> to_bool)) with _ -> Some default