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
530 lines
22 KiB
OCaml
530 lines
22 KiB
OCaml
(* Server-side React UI components using server-reason-react *)
|
|
|
|
open Dream
|
|
open Lwt.Infix
|
|
open Database
|
|
|
|
(* HTML helpers *)
|
|
let html ?(title="Website Monitor") ?(body="") ?(extra_head="") () =
|
|
Printf.sprintf {|
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>%s</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
%s
|
|
<style>
|
|
body { font-family: 'Inter', sans-serif; }
|
|
.status-healthy { color: #10b981; }
|
|
.status-unhealthy { color: #ef4444; }
|
|
.status-unknown { color: #6b7280; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen">
|
|
<nav class="bg-white shadow-sm border-b">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between h-16">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0 flex items-center">
|
|
<i class="fas fa-satellite-dish text-blue-600 text-2xl mr-2"></i>
|
|
<span class="font-bold text-xl text-gray-900">Website Monitor</span>
|
|
</div>
|
|
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
|
|
<a href="/dashboard" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">Dashboard</a>
|
|
<a href="/dashboard/websites" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">Websites</a>
|
|
<a href="/dashboard/alerts" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">Alerts</a>
|
|
<a href="/dashboard/settings" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">Settings</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
%s
|
|
</main>
|
|
<script>
|
|
// Auto-refresh functionality
|
|
function refreshPage() {
|
|
location.reload();
|
|
}
|
|
// Refresh every 60 seconds
|
|
setInterval(refreshPage, 60000);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|} title extra_head body
|
|
|
|
(* Dashboard page *)
|
|
let serve_dashboard req =
|
|
Websites.get_all ()
|
|
>>= fun websites ->
|
|
let active_websites = List.filter (fun w -> w.active) websites in
|
|
let healthy_count =
|
|
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 active_websites
|
|
in
|
|
let total_active = List.length active_websites in
|
|
|
|
let websites_cards =
|
|
List.map
|
|
(fun w ->
|
|
let status_icon =
|
|
match w.last_status with
|
|
| None -> "<i class='fas fa-question-circle text-gray-400'></i>"
|
|
| Some status ->
|
|
if status = w.expected_status then
|
|
"<i class='fas fa-check-circle text-green-500'></i>"
|
|
else
|
|
"<i class='fas fa-exclamation-circle text-red-500'></i>"
|
|
in
|
|
let last_checked =
|
|
match w.last_checked with
|
|
| None -> "Never"
|
|
| Some t ->
|
|
try
|
|
let t' = Ptime.v (Unix.gettimeofday ()) in
|
|
let diff = Ptime.diff t' t |> Ptime.Span.to_float_s in
|
|
if diff < 60.0 then Printf.sprintf "%.0f seconds ago" diff
|
|
else if diff < 3600.0 then Printf.sprintf "%.0f minutes ago" (diff /. 60.0)
|
|
else Printf.sprintf "%.1f hours ago" (diff /. 3600.0)
|
|
with _ -> "Unknown"
|
|
in
|
|
Printf.sprintf {|
|
|
<div class="bg-white rounded-lg shadow-sm border p-6">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex items-start space-x-4">
|
|
<div class="text-2xl">%s</div>
|
|
<div>
|
|
<h3 class="text-lg font-medium text-gray-900">%s</h3>
|
|
<p class="text-sm text-gray-500 truncate">%s</p>
|
|
</div>
|
|
</div>
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium %s">
|
|
%s
|
|
</span>
|
|
</div>
|
|
<div class="mt-4 flex items-center justify-between text-sm">
|
|
<span class="text-gray-500">Last checked: %s</span>
|
|
<a href="/dashboard/websites" class="text-blue-600 hover:text-blue-800">View Details <i class="fas fa-arrow-right ml-1"></i></a>
|
|
</div>
|
|
</div>
|
|
|}
|
|
status_icon
|
|
w.name
|
|
w.url
|
|
(if w.active then "bg-green-100 text-green-800" else "bg-gray-100 text-gray-800")
|
|
(if w.active then "Active" else "Inactive")
|
|
last_checked
|
|
)
|
|
websites
|
|
|> String.concat "\n"
|
|
in
|
|
|
|
let body = Printf.sprintf {|
|
|
<div class="space-y-6">
|
|
<div class="flex items-center justify-between">
|
|
<h1 class="text-3xl font-bold text-gray-900">Dashboard</h1>
|
|
<button onclick="window.location='/dashboard/websites'" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
|
|
<i class="fas fa-plus mr-2"></i>Add Website
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
<div class="bg-white rounded-lg shadow-sm border p-6">
|
|
<div class="flex items-center">
|
|
<div class="p-3 rounded-md bg-blue-100">
|
|
<i class="fas fa-globe text-blue-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-gray-500">Total Websites</p>
|
|
<p class="text-2xl font-semibold text-gray-900">%d</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm border p-6">
|
|
<div class="flex items-center">
|
|
<div class="p-3 rounded-md bg-green-100">
|
|
<i class="fas fa-play text-green-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-gray-500">Active</p>
|
|
<p class="text-2xl font-semibold text-gray-900">%d</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm border p-6">
|
|
<div class="flex items-center">
|
|
<div class="p-3 rounded-md bg-green-100">
|
|
<i class="fas fa-check-circle text-green-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-gray-500">Healthy</p>
|
|
<p class="text-2xl font-semibold text-gray-900">%d</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm border p-6">
|
|
<div class="flex items-center">
|
|
<div class="p-3 rounded-md bg-red-100">
|
|
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-gray-500">Unhealthy</p>
|
|
<p class="text-2xl font-semibold text-gray-900">%d</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Website Status</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
%s
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|} (List.length websites) total_active healthy_count (total_active - healthy_count) websites_cards
|
|
in
|
|
|
|
let html_content = html ~title:"Website Monitor - Dashboard" ~body () in
|
|
Lwt.return (Dream.html html_content)
|
|
|
|
(* Websites management page *)
|
|
let serve_websites_page req =
|
|
Websites.get_all ()
|
|
>>= fun websites ->
|
|
|
|
let website_rows =
|
|
List.map
|
|
(fun w ->
|
|
let status_badge =
|
|
match w.last_status with
|
|
| None -> "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800'>Unknown</span>"
|
|
| Some status ->
|
|
if status = w.expected_status then
|
|
"<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800'>OK</span>"
|
|
else
|
|
Printf.sprintf "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800'>%d</span>" status
|
|
in
|
|
let active_badge =
|
|
if w.active then
|
|
"<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800'>Active</span>"
|
|
else
|
|
"<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800'>Inactive</span>"
|
|
in
|
|
Printf.sprintf {|
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">%s</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">%s</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">%s</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">%s</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">%s</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">%d</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
<button onclick="checkWebsite(%Ld)" class="text-blue-600 hover:text-blue-900 mr-3">Check Now</button>
|
|
<button onclick="editWebsite(%Ld)" class="text-indigo-600 hover:text-indigo-900 mr-3">Edit</button>
|
|
<button onclick="deleteWebsite(%Ld)" class="text-red-600 hover:text-red-900">Delete</button>
|
|
</td>
|
|
</tr>
|
|
|}
|
|
w.name
|
|
w.url
|
|
status_badge
|
|
active_badge
|
|
(match w.last_checked with
|
|
| None -> "Never"
|
|
| Some t -> Ptime.to_rfc3339 t)
|
|
w.check_interval
|
|
w.id
|
|
w.id
|
|
w.id
|
|
)
|
|
websites
|
|
|> String.concat "\n"
|
|
in
|
|
|
|
let body = Printf.sprintf {|
|
|
<div class="space-y-6">
|
|
<div class="flex items-center justify-between">
|
|
<h1 class="text-3xl font-bold text-gray-900">Websites</h1>
|
|
<button onclick="openAddModal()" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
|
|
<i class="fas fa-plus mr-2"></i>Add Website
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bg-white shadow-sm border rounded-lg overflow-hidden">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Active</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Checked</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Interval (s)</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
%s
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function checkWebsite(id) {
|
|
fetch('/api/websites/' + id + '/check', { method: 'POST' })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
alert('Website check initiated');
|
|
setTimeout(() => location.reload(), 2000);
|
|
})
|
|
.catch(err => alert('Error: ' + err));
|
|
}
|
|
|
|
function openAddModal() {
|
|
alert('Add website modal - Implementation pending');
|
|
}
|
|
|
|
function editWebsite(id) {
|
|
alert('Edit website ' + id + ' - Implementation pending');
|
|
}
|
|
|
|
function deleteWebsite(id) {
|
|
if (confirm('Are you sure you want to delete this website?')) {
|
|
fetch('/api/websites/' + id, { method: 'DELETE' })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Error: ' + data.error);
|
|
}
|
|
})
|
|
.catch(err => alert('Error: ' + err));
|
|
}
|
|
}
|
|
</script>
|
|
|} website_rows
|
|
in
|
|
|
|
let html_content = html ~title:"Website Monitor - Websites" ~body () in
|
|
Lwt.return (Dream.html html_content)
|
|
|
|
(* Alerts management page *)
|
|
let serve_alerts_page req =
|
|
Alerts.get_all ()
|
|
>>= fun alerts ->
|
|
|
|
let alert_rows =
|
|
List.map
|
|
(fun a ->
|
|
let type_badge =
|
|
match a.alert_type with
|
|
| "email" -> "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800'><i class='fas fa-envelope mr-1'></i>Email</span>"
|
|
| "webhook" -> "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800'><i class='fas fa-link mr-1'></i>Webhook</span>"
|
|
| _ -> Printf.sprintf "<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800'>%s</span>" a.alert_type
|
|
in
|
|
let enabled_badge =
|
|
if a.enabled then
|
|
"<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800'>Enabled</span>"
|
|
else
|
|
"<span class='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800'>Disabled</span>"
|
|
in
|
|
Printf.sprintf {|
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">%Ld</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">%s</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">%s</td>
|
|
<td class="px-6 py-4 text-sm text-gray-500 max-w-xs truncate"><code class="bg-gray-100 px-1 rounded">%s</code></td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">%s</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
<button onclick="testAlert(%Ld)" class="text-blue-600 hover:text-blue-900 mr-3">Test</button>
|
|
<button onclick="editAlert(%Ld)" class="text-indigo-600 hover:text-indigo-900 mr-3">Edit</button>
|
|
<button onclick="deleteAlert(%Ld)" class="text-red-600 hover:text-red-900">Delete</button>
|
|
</td>
|
|
</tr>
|
|
|}
|
|
a.website_id
|
|
type_badge
|
|
enabled_badge
|
|
a.config
|
|
(Ptime.to_rfc3339 a.created_at)
|
|
a.id
|
|
a.id
|
|
a.id
|
|
)
|
|
alerts
|
|
|> String.concat "\n"
|
|
in
|
|
|
|
let body = Printf.sprintf {|
|
|
<div class="space-y-6">
|
|
<div class="flex items-center justify-between">
|
|
<h1 class="text-3xl font-bold text-gray-900">Alerts</h1>
|
|
<button onclick="openAddModal()" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
|
|
<i class="fas fa-plus mr-2"></i>Add Alert
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bg-white shadow-sm border rounded-lg overflow-hidden">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Website ID</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Config</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
%s
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function testAlert(id) {
|
|
fetch('/api/alerts/' + id + '/test', { method: 'POST' })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert('Test alert sent successfully!');
|
|
} else {
|
|
alert('Error: ' + data.error);
|
|
}
|
|
})
|
|
.catch(err => alert('Error: ' + err));
|
|
}
|
|
|
|
function openAddModal() {
|
|
alert('Add alert modal - Implementation pending');
|
|
}
|
|
|
|
function editAlert(id) {
|
|
alert('Edit alert ' + id + ' - Implementation pending');
|
|
}
|
|
|
|
function deleteAlert(id) {
|
|
if (confirm('Are you sure you want to delete this alert?')) {
|
|
fetch('/api/alerts/' + id, { method: 'DELETE' })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Error: ' + data.error);
|
|
}
|
|
})
|
|
.catch(err => alert('Error: ' + err));
|
|
}
|
|
}
|
|
</script>
|
|
|} alert_rows
|
|
in
|
|
|
|
let html_content = html ~title:"Website Monitor - Alerts" ~body () in
|
|
Lwt.return (Dream.html html_content)
|
|
|
|
(* Settings page *)
|
|
let serve_settings_page req =
|
|
let body = Printf.sprintf {|
|
|
<div class="space-y-6">
|
|
<h1 class="text-3xl font-bold text-gray-900">Settings</h1>
|
|
|
|
<div class="bg-white shadow-sm border rounded-lg">
|
|
<div class="px-6 py-4 border-b">
|
|
<h2 class="text-lg font-medium text-gray-900">Monitoring Settings</h2>
|
|
</div>
|
|
<div class="p-6">
|
|
<form class="space-y-6">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">Default Check Interval (seconds)</label>
|
|
<input type="number" value="300" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">Default Timeout (seconds)</label>
|
|
<input type="number" value="30" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">History Retention (days)</label>
|
|
<input type="number" value="30" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
|
</div>
|
|
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
|
|
Save Settings
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white shadow-sm border rounded-lg">
|
|
<div class="px-6 py-4 border-b">
|
|
<h2 class="text-lg font-medium text-gray-900">Email Configuration</h2>
|
|
</div>
|
|
<div class="p-6">
|
|
<form class="space-y-6">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">SMTP Host</label>
|
|
<input type="text" placeholder="smtp.gmail.com" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">SMTP Port</label>
|
|
<input type="number" placeholder="587" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">SMTP Username</label>
|
|
<input type="text" placeholder="your-email@example.com" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">SMTP Password</label>
|
|
<input type="password" placeholder="••••••••" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
|
</div>
|
|
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
|
|
Save Email Settings
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white shadow-sm border rounded-lg">
|
|
<div class="px-6 py-4 border-b">
|
|
<h2 class="text-lg font-medium text-gray-900">System Information</h2>
|
|
</div>
|
|
<div class="p-6">
|
|
<dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2">
|
|
<div>
|
|
<dt class="text-sm font-medium text-gray-500">Version</dt>
|
|
<dd class="mt-1 text-sm text-gray-900">1.0.0</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-sm font-medium text-gray-500">Environment</dt>
|
|
<dd class="mt-1 text-sm text-gray-900">Production</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-sm font-medium text-gray-500">Scheduler Status</dt>
|
|
<dd class="mt-1 text-sm text-gray-900">Running</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-sm font-medium text-gray-500">Database</dt>
|
|
<dd class="mt-1 text-sm text-gray-900">Connected</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|}
|
|
in
|
|
|
|
let html_content = html ~title:"Website Monitor - Settings" ~body () in
|
|
Lwt.return (Dream.html html_content)
|