Introduction
This project is a simple Bash-based Linux Health Checks monitoring script.
It collects server metrics such as CPU, memory, disk, swap, and URL status, then generates an HTML dashboard report indicating whether the system is OK, Warning, or Critical.
Prerequisites
- Linux system (Ubuntu/CentOS/RHEL)
- Basic Bash scripting knowledge
- Commands:
top,free,df,curl,awk - Web browser to view the HTML report
ALSO READ:
- Complete Install Kubernetes with Kind (Kubernetes IN Docker) on RHEL 9 / CentOS Stream 9
- AWS S3 Backups with This Efficient Shell Script
- Bash Brackets Explained in Simple Words (With 8 Examples)
Click here to go to the GitHub repos link
Features
- CPU usage check
- Memory usage check
- Swap usage check
- Disk utilization check
- URL/website status check
- Automatic status classification (OK / Warning / Critical)
- Interactive HTML dashboard with filters and details
- Can be scheduled using cron
Health Checks Work:
- Collects system data using Linux commands
- Compares values with thresholds
- Assigns health status
- Generates a styled HTML report
Health Check Shell Script
#!/bin/bash
# Time and output file
date_stamp=$(date +'%F')
report="/home/devuser/techbasehub_scripts/health_cecks_script/v2health_checks_reprot_$date_stamp.html"
# --------- GENERATE HTML ---------
cat > "$report" <<EOF
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Health checks</title>
<style>
/* ---------- Base ---------- */
body {
font-family: Arial, sans-serif;
padding: 40px;
padding-top: 5px;
background-color: #F4F4F4;
color: #222;
}
/* Top header row */
.header-row {
width: 100%;
overflow: hidden;
font-weight: bold;
color: #444;
}
.left { float: left; }
.right { float: right; }
hr {
border: none;
border-top: 1px solid #E0E0E0;
margin: 12px 0 20px;
}
/* ---------- Title box ---------- */
.box {
width: 550px;
margin: 0 auto; /* centers the box */
padding: 16px 20px;
border: 2px solid #0078D4; /* Microsoft blue border */
border-radius: 10px;
text-align: center;
font-size: 20px;
font-weight: bold;
background: #FFFFFF;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
color: #222;
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.box:hover {
border-color: #005A9E; /* slightly darker blue */
box-shadow: 0 8px 20px rgba(0,0,0,0.25);
transform: translateY(-2px);
cursor: default; /* or pointer if you plan to make it clickable */
}
/* ---------- THREE BOXES SIDE BY SIDE (equal size) ---------- */
.row3 {
display: flex;
gap: 16px;
align-items: stretch; /* equal visual height */
flex-wrap: nowrap; /* keep side-by-side */
width: 100%;
}
/* Make all three boxes equal width/height and nicely aligned */
.instance-box,
.status-box,
.summary-box {
flex: 1; /* equal width */
min-width: 250px;
display: flex;
flex-direction: column;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0,0,0,0.12);
padding: 16px 20px;
}
/* Instance Box (left) */
.instance-box {
border: 2px solid #0078D4;
background: #FFFFFF;
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
}
.instance-box:hover {
border-color: #005A9E;
background-color: #F3F8FF; /* very light blue */
box-shadow: 0 8px 20px rgba(0,0,0,0.20);
transform: translateY(-2px);
cursor: default; /* use pointer if you make it clickable later */
}
.instance-box .meta {
text-align: left;
font-size: 14px;
line-height: 1.45;
white-space: nowrap;
margin: auto 0; /* vertical centering of content */
}
/* Overall Health (center) - base */
.status-box {
border: 2px solid #F59E0B;
background: #FFF7ED;
color: #9A3412;
font-weight: 600;
text-align: center;
justify-content: center; /* vertical centering */
align-items: center; /* horizontal centering */
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
}
.status-box:hover {
box-shadow: 0 8px 20px rgba(0,0,0,0.20);
transform: translateY(-2px);
cursor: default;
}
.status-box .label {
display: block;
font-size: 13px;
text-transform: uppercase;
margin-bottom: 6px;
}
.status-box .value {
font-size: 22px;
font-weight: 800;
}
/* Overall Health variants (auto-applied) */
.status-box.ok {
border-color: #0FA958;
background: #E6F9EE;
color: #065F46;
}
.status-box.warning {
border-color: #F59E0B;
background: #FFF7ED;
color: #9A3412;
}
.status-box.critical {
border-color: #D62828;
background: #FEECEC;
color: #7F1D1D;
}
/* Summary Box (right) */
.summary-box {
border: 2px solid #0078D4; /* Blue border */
background: #FFFFFF;
color: #222;
font-weight: 600;
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
}
.summary-box:hover {
border-color: #005A9E;
background-color: #F3F8FF; /* very light blue */
box-shadow: 0 8px 20px rgba(0,0,0,0.20);
transform: translateY(-2px);
cursor: default;
}
.summary-box .label {
display: block;
font-size: 15px;
text-transform: uppercase;
margin-bottom: 10px;
color: #0078D4; /* Blue heading */
}
/* Summary filter buttons (side-by-side) */
.summary-actions {
display: flex;
gap: 8px;
flex-wrap: wrap; /* wrap if narrow */
align-items: center;
}
.summary-filter {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
font-size: 14px;
border-radius: 999px; /* pill */
border: 1px solid #D1D5DB;
background: #F8FAFC;
color: #111827;
cursor: pointer;
user-select: none;
outline: none;
}
.summary-filter .dot {
width: 10px; height: 10px; border-radius: 50%;
display: inline-block;
}
.summary-filter.ok { color: #065F46; background: #ECFDF5; border-color: #A7F3D0; }
.summary-filter.warning { color: #92400E; background: #FFF7ED; border-color: #FED7AA; }
.summary-filter.critical { color: #7F1D1D; background: #FEF2F2; border-color: #FECACA; }
.summary-filter.active { box-shadow: 0 0 0 2px #0078D4 inset; }
/* ---------- Health table ---------- */
.health-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
background: #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.health-table th {
background: #0078D4; /* Microsoft blue */
color: white;
padding: 12px;
text-align: left;
font-size: 15px;
}
.health-table td {
padding: 12px;
font-size: 14px;
color: #333;
border-bottom: 1px solid #EEE;
}
/* Status badges inside table */
.status-ok {
color: #0FA958;
background: #E6F9EE;
padding: 4px 10px;
border-radius: 5px;
display: inline-block;
font-weight: bold;
}
.status-warning {
color: #E29A00;
background: #FFF4E0;
padding: 4px 10px;
border-radius: 5px;
display: inline-block;
font-weight: bold.
}
.status-critical {
color: #D62828;
background: #FEECEC;
padding: 4px 10px;
border-radius: 5px;
display: inline-block;
font-weight: bold;
}
/* Buttons (used by Details) */
.detail-btn {
box-shadow: 1px 1px black;
border-radius: 15px;
padding: 10px 16px;
font-size: 16px;
cursor: pointer;
border: 1px solid #D1D5DB;
background: #E5E7EB;
}
button:hover, .detail-btn:hover {
background: #D1D5DB;
}
/* ====== ONLY CHANGE ADDED EARLIER: Color styling for the Details button ====== */
.detail-btn {
background-color: #0078D4 !important; /* Microsoft blue */
color: #FFFFFF !important;
border: none !important;
border-radius: 6px !important;
padding: 8px 16px !important;
font-weight: bold;
}
.detail-btn:hover {
background-color: #005A9E !important; /* darker blue on hover */
}
/* ====== END CHANGE ====== */
/* ---------- Inline details rows inside the table ---------- */
.details-row { display: none; }
.details-row.show { display: table-row; }
.details-row td {
padding: 0;
background: #FAFAFA;
border-bottom: 1px solid #EEE;
}
.details-content { padding: 12px 16px; }
/* Inner table inside details */
.inner-table {
width: 100%;
border-collapse: collapse;
margin-top: 8px;
background: #fff;
border: 1px solid #EEE;
border-radius: 8px;
overflow: hidden;
}
.inner-table th {
background: #F3F4F6;
padding: 8px 10px;
text-align: left;
font-size: 14px;
color: #111827;
}
.inner-table td {
padding: 8px 10px;
border-top: 1px solid #F1F1F1;
color: #333;
}
.footer-bar {
background: #E5E7EB;
padding: 10px;
text-align: center;
border-radius: 8px;
margin-top: 20px;
font-size: 14px;
font-weight: bold;
}
/* .top-header {
background: linear-gradient(to right, #0078D4, #00A1FF);
color: white;
padding: 20px;
border-radius: 12px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
} */
.top-header {
padding: 15px;
display: flex;
justify-content: space-between;
margin-bottom: 20px;
box-shadow: 5px 5px 15px rgba(0, 0, 0, 0.4);
}
/* hover effect (optional) */
.top-header img:hover {
transform: scale(1.05);
}
/* ====== ONLY ADDED: Hover colors for Summary buttons ====== */
.summary-filter.ok:hover {
background:#D1FAE5 !important;
border-color:#6EE7B7 !important;
}
.summary-filter.warning:hover {
background:#FFE8C7 !important;
border-color:#FBBF77 !important;
}
.summary-filter.critical:hover {
background:#FEE2E2 !important;
border-color:#FCA5A5 !important;
}
/* Server heading style */
.server-heading {
font-size: 16px;
font-weight: bold;
margin-bottom: 6px;
color: #0078D4; /* Microsoft blue */
}
/* ====== END ====== */
</style>
</head>
<body>
<div class="top-header">
<div><img src="C:\Devsecops\repos\test/google.png" alt="Google Logo" height="30"></div>
<div><img src="C:\Devsecops\repos\test/tech_base_hub.png" alt="Tech Base Hub Logo" height="30"></div>
</div>
<hr>
<!-- Title box -->
<div class="box">
Health Checks Report - $(hostname)
</div>
<hr>
<!-- THREE BOXES SIDE BY SIDE (equal size) -->
<div class="row3">
<!-- LEFT -->
<div class="instance-box">
<div class="meta">
<div class="server-heading">Server Details</div>
<div><b>Instance:</b>$(hostname)</div>
<div><b>Time Stamp:</b>$(date)</div>
</div>
</div>
<!-- CENTER: Overall Health (auto) -->
<div id="overallBox" class="status-box">
<span class="label">Overall Health</span>
<span id="overallValue" class="value">—</span>
</div>
<!-- RIGHT: Summary with side-by-side filter buttons -->
<div class="summary-box">
<span class="label">Summary</span>
<div class="summary-actions">
<button class="summary-filter ok" data-filter="ok" aria-pressed="false">
<span class="dot" style="background:#0FA958"></span>
OK : <span id="okCount">0</span>
</button>
<button class="summary-filter warning" data-filter="warning" aria-pressed="false">
<span class="dot" style="background:#E29A00"></span>
Warning : <span id="warnCount">0</span>
</button>
<button class="summary-filter critical" data-filter="critical" aria-pressed="false">
<span class="dot" style="background:#D62828"></span>
Critical : <span id="critCount">0</span>
</button>
</div>
</div>
</div>
<hr>
EOF
cat >> "$report" <<EOF
<!-- HEALTH TABLE -->
<table class="health-table" id="healthTable">
<tr>
<th>S.No</th>
<th>Check</th>
<th>Status</th>
<th>Details</th>
</tr>
EOF
# --------- CPU CALCULATION (dynamic) ---------
# Get CPU usage (user + system) as integer percentage
cpu_usage=$(top -bn 1 | awk '/%Cpu/ {print int($2 + $4)}')
# Decide status based on usage
if [ "$cpu_usage" -le 30 ]; then
cpu_css="status-ok"
cpu_status="ok"
cpu_status_cap="OK"
elif [ "$cpu_usage" -le 40 ]; then
cpu_css="status-warning"
cpu_status="warning"
cpu_status_cap="WARNING"
else
cpu_css="status-critical"
cpu_status="critical"
cpu_status_cap="CRITICAL"
fi
cat >> "$report" <<EOF
<!-- CPU -->
<tr class="data-row" data-status="$cpu_status">
<td>1</td>
<td><b>CPU Usage</b></td>
<td><span class="$cpu_css">$cpu_status_cap</span></td>
<td><button class="detail-btn">Details</button></td>
</tr>
<tr class="details-row">
<td colspan="4">
<div class="details-content">
<h4><u>CPU Usage</u></h4>
<table class="inner-table">
<tr><th>Processor %</th><th>User %</th><th>System %</th><th>Idle %</th></tr>
$(top -bn 1 | awk '/%Cpu/ {printf "<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>", $2, $4, $6, $8}')
</table>
</div>
</td>
</tr>
EOF
# --------- Memory CALCULATION ---------
# --------- Memory CALCULATION ---------
total=$(free -m | awk '/Mem:/ {print $2}')
available=$(free -m | awk '/Mem:/ {print $7}')
used=$((total - available))
# Prevent division by zero (very rare but safer)
# [ "$total" -eq 0 ] && total=1
mem_percent=$((used * 100 / total))
echo "Memory usage: $mem_percent%"
# Reasonable thresholds for a real server
if [ "$mem_percent" -ge 20 ]; then
mem_css="status-critical"
mem_status="critical"
mem_status_cap="CRITICAL"
elif [ "$mem_percent" -ge 10 ]; then
mem_css="status-warning"
mem_status="warning"
mem_status_cap="WARNING"
else
mem_css="status-ok"
mem_status="ok"
mem_status_cap="OK"
fi
cat >> "$report" <<EOF
<!-- Memory -->
<tr class="data-row" data-status="$mem_status">
<td>2</td>
<td><b>Memory Usage</b></td>
<td><span class="$mem_css">$mem_status_cap</span></td>
<td><button class="detail-btn">Details</button></td>
</tr>
<tr class="details-row">
<td colspan="4">
<div class="details-content">
<h4><u>Memory Usage</u></h4>
<table class="inner-table">
<tr><th></th><th>Total</th><th>Used</th><th>Free</th><th>Shared</th><th>Buff/Cache</th><th>Available</th></tr>
$(free -h | awk 'NR==2 {print "<tr><th>"$1"</th><td>"$2"</td><td>"$3"</td><td>"$4"</td><td>"$5"</td><td>"$6"</td><td>"$7"</td></tr>"}')
</table>
</div>
</td>
</tr>
EOF
# --------- Swap CALCULATION ---------
# Get total swap (in bytes)
swap_total=$(free | awk '/Swap:/ {print $2}')
# Get used swap (in bytes)
swap_used=$(free | awk '/Swap:/ {print $3}')
# Calculate percentage (integer)
swap_percentage=$(( (swap_used * 100) / swap_total ))
# Show result
echo "Swap usage $swap_percentage%"
if [ "$swap_percentage" -gt 90 ];
then
swap_css="status-critical"
swap_status="critical"
swap_status_cap="CRITICAL"
elif [ "$swap_percentage" -gt 80 ];
then
swap_css="status-warning"
swap_status="warning"
swap_status_cap="WARNING"
else
swap_css="status-ok"
swap_status="ok"
swap_status_cap="OK"
fi
cat >> "$report" <<EOF
<!-- Swap -->
<tr class="data-row" data-status="$swap_status">
<td>2</td>
<td><b>Swap Usage</b></td>
<td><span class="$swap_css">$swap_status_cap</span></td>
<td><button class="detail-btn">Details</button></td>
</tr>
<tr class="details-row">
<td colspan="4">
<div class="details-content">
<h4><u>Swap Usage</u></h4>
<table class="inner-table">
<tr><th></th><th>Total</th><th>Used</th><th>Free</th></tr>
$(free -h | awk 'NR==3 {print "<tr><th>"$1"</th><td>"$2"</td><td>"$3"</td><td>"$4"</td></tr>"}')
</table>
</div>
</td>
</tr>
EOF
# --------- Disk usage CALCULATION (dynamic) ---------
disk_cri=$(df -h | grep -vE '^Filesystem' | awk '$5+0 >= 70')
disk_war=$(df -h | grep -vE '^Filesystem' | awk '$5+0 >= 30')
if [ -n "$disk_cri" ];
then
disk_css="status-critical"
disk_status="critical"
disk_status_cap="CRITICAL"
echo "$disk_cri"
echo "critical"
elif [ -n "$disk_war" ];
then
disk_css="status-warning"
disk_status="warning"
disk_status_cap="WARNING"
echo "$disk_war"
echo "warning"
else
disk_css="status-ok"
disk_status="ok"
disk_status_cap="OK"
fi
cat >> "$report" <<EOF
<!-- Disk -->
<tr class="data-row" data-status="$disk_status">
<td>3</td>
<td><b>Disk Usage</b></td>
<td><span class="$disk_css">$disk_status_cap</span></td>
<td><button class="detail-btn">Details</button></td>
</tr>
<tr class="details-row">
<td colspan="4">
<div class="details-content">
<h4><u>Disk Usage</u></h4>
<table class="inner-table">
<tr><th>Filesystem</th><th>Type</th><th>Size</th><th>Used</th><th>Avail</th><th>Use%</th></th><th>Mounted on</th></tr>
$(df -Th | awk 'NR>1 {print "<tr><td>"$1"</td><td>"$2"</td><td>"$3"</td><td>"$4"</td><td>"$5"</td><td>"$6"</td><td>"$7"</td></tr>"}')
</table>
</div>
</td>
</tr>
EOF
# --------- Disk usage CALCULATION (dynamic) ---------
url_data=$(curl -sI https://www.google.com/)
url_status=$(curl -sI https://www.google.com/ | awk 'NR==1 {print int($2)}')
if [ "$url_status" -eq 200 ];
then
url_css="status-ok"
url_status="ok"
url_status_cap="OK"
else
url_css="status-critical"
url_status="critical"
url_status_cap="CRITICAL"
fi
cat >> "$report" <<EOF
<!-- URL -->
<tr class="data-row" data-status="$url_status">
<td>4</td>
<td><b>URL Status</b></td>
<td><span class="$url_css">$url_status_cap</span></td>
<td><button class="detail-btn">Details</button></td>
</tr>
<tr class="details-row">
<td colspan="4">
<div class="details-content">
<h4><u>URL Status</u></h4>
<table class="inner-table">
<tr><td><pre>$url_data</pre></td></tr>
</table>
</div>
</td>
</tr>
EOF
cat >> "$report" <<EOF
</table>
EOF
cat >> "$report" <<EOF
<!-- Modern Footer -->
<footer class="footer-bar">
© 2026 Tech Base Hub. All rights reserved.
</footer>
<script>
/* ---------- Inline Details Toggle ---------- */
document.addEventListener('click', function (e) {
const btn = e.target.closest('.detail-btn');
if (!btn) return;
const tr = btn.closest('tr');
const detailsRow = tr && tr.nextElementSibling;
if (!detailsRow || !detailsRow.classList.contains('details-row')) return;
const isShown = detailsRow.classList.toggle('show');
// FIX: sync inline 'display' with .show class so filters can't suppress it
detailsRow.style.display = isShown ? 'table-row' : 'none';
btn.textContent = isShown ? 'Hide' : 'Details';
// Keep only one details row open at a time
document.querySelectorAll('.details-row.show').forEach(row => {
if (row !== detailsRow) {
row.classList.remove('show');
row.style.display = 'none'; // FIX: ensure others are actually hidden
const otherBtn = row.previousElementSibling?.querySelector('.detail-btn');
if (otherBtn) otherBtn.textContent = 'Details';
}
});
});
/* ---------- Summary Counts, Filters & Overall Health ---------- */
(function(){
let activeFilter = null; // 'ok' | 'warning' | 'critical' | null
const table = document.getElementById('healthTable');
const dataRows = () => Array.from(table.querySelectorAll('tr.data-row'));
// Count statuses from the table
function getCounts() {
const counts = { ok: 0, warning: 0, critical: 0 };
dataRows().forEach(tr => {
const st = tr.getAttribute('data-status');
if (counts[st] !== undefined) counts[st]++;
});
return counts;
}
// Update the Summary numbers
function updateSummaryCounts(counts) {
document.getElementById('okCount').textContent = counts.ok;
document.getElementById('warnCount').textContent = counts.warning;
document.getElementById('critCount').textContent = counts.critical;
}
// Set Overall Health by counts with tie-break: critical > warning > ok
function updateOverallHealth(counts) {
const overallBox = document.getElementById('overallBox');
const overallText = document.getElementById('overallValue');
const maxVal = Math.max(counts.ok, counts.warning, counts.critical);
let state = 'ok';
if (counts.critical === maxVal) {
state = 'critical';
} else if (counts.warning === maxVal) {
state = 'warning';
} else {
state = 'ok';
}
overallText.textContent = state.toUpperCase();
overallBox.classList.remove('ok', 'warning', 'critical');
overallBox.classList.add(state);
}
// Apply/toggle filter driven by Summary clicks
function applyFilter(filterKey) {
if (activeFilter === filterKey) {
activeFilter = null; // toggle off -> show all
} else {
activeFilter = filterKey;
}
// Active state styling
document.querySelectorAll('.summary-filter').forEach(btn => {
const f = btn.getAttribute('data-filter');
const isActive = (activeFilter === f);
btn.classList.toggle('active', isActive);
btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
});
// Show/hide rows based on filter (and manage their details row)
dataRows().forEach(tr => {
const status = tr.getAttribute('data-status');
const show = !activeFilter || status === activeFilter;
tr.style.display = show ? '' : 'none';
const det = tr.nextElementSibling;
if (det && det.classList.contains('details-row')) {
if (!show) {
det.classList.remove('show');
det.style.display = 'none';
const btn = tr.querySelector('.detail-btn');
if (btn) btn.textContent = 'Details';
} else {
// If row is visible and details were open, display them
det.style.display = det.classList.contains('show') ? 'table-row' : 'none';
}
}
});
}
// Wire summary buttons
document.querySelectorAll('.summary-filter').forEach(btn => {
btn.addEventListener('click', () => applyFilter(btn.getAttribute('data-filter')));
});
// Initial compute
const counts = getCounts();
updateSummaryCounts(counts);
updateOverallHealth(counts);
})();
</script>
</body>
</html>
EOF
echo "Report generated: $report"
Note: Please update thresholds, paths, and URLs as per your server environment before running the script.
