Files
aitest-reasonml-mon-fbk/lib/api.ml
Charles N Wyble e1ff581603 feat: initial commit - complete website monitoring application
Build a comprehensive website monitoring application with ReasonML, OCaml, and server-reason-react.

Features:
- Real-time website monitoring with HTTP status checks
- Email and webhook alerting system
- Beautiful admin dashboard with Tailwind CSS
- Complete REST API for CRUD operations
- Background monitoring scheduler
- Multi-container Docker setup with 1-core CPU constraint
- PostgreSQL database with Caqti
- Full documentation and setup guides

Tech Stack:
- OCaml 5.0+ with ReasonML
- Dream web framework
- server-reason-react for UI
- PostgreSQL 16 database
- Docker & Docker Compose

Files:
- 9 OCaml source files (1961 LOC)
- 6 documentation files (1603 LOC)
- Complete Docker configuration
- Comprehensive API documentation

💘 Generated with Crush
2026-01-13 15:56:42 -05:00

414 lines
14 KiB
OCaml

(* 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