From 5c798c075da49dadb8384695f57a4dfabe1cf05d Mon Sep 17 00:00:00 2001 From: MayaChat Date: Mon, 24 Nov 2025 00:03:14 -0500 Subject: [PATCH] Add automatic health checks and status indicators for services --- README.md | 35 +++++++++++++++++++++----------- index.html | 56 ++++++++++++++++++++++++++++++++++++++++++++++------ services.xml | 23 ++++++++++++++------- styles.css | 2 ++ 4 files changed, 92 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index a6218dc..8d6be95 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A lightweight, self-hosted dashboard for quick access to your Docker services wi - ℹ️ **Connection Details** - Info button shows hostname/port for each service - 🎮 **Keyboard Navigation** - Arrow keys to navigate, Enter to open, Esc to clear - 🟢 **Status Indicators** - Optional visual status (online/offline/maintenance) +- 🏥 **Automatic Health Checks** - Real-time ping tests to detect service availability ## Quick Start @@ -68,7 +69,8 @@ docker-compose restart | `port` | No | Port number | `"8080"` | | `host` | No | Custom hostname or full URL | `"nextcloud.example.com"` | | `logo` | No | Icon filename in `/logos/` | `"nextcloud.svg"` | -| `status` | No | Service status indicator | `"online"`, `"offline"`, or `"maintenance"` | +| `status` | No | Set to `"maintenance"` to skip health check | `"maintenance"` | +| `check-health` | No | Enable/disable auto health check | `"true"` (default) or `"false"` | ### URL Resolution Logic @@ -115,23 +117,34 @@ When a service has both a hostname and port configured, a small info button (ⓘ ## Status Indicators -Add optional status indicators to services by including a `status` attribute: +Services are automatically checked for availability when the page loads. Status indicators appear in the top-right corner of each card: ```xml - - + + - - - - + + + + ``` Status colors: -- **Green** (online) - Service is running normally, pulsing animation -- **Red** (offline) - Service is not available -- **Orange** (maintenance) - Service is under maintenance + +- **Gray spinning** (checking) - Currently testing service availability +- **Green pulsing** (online) - Service responded successfully to health check +- **Red** (offline) - Service failed to respond or timed out (5 seconds) +- **Orange** (maintenance) - Manual maintenance mode, health check skipped + +### How Health Checks Work + +- Each service is automatically pinged when the page loads +- Uses a 5-second timeout per service +- Checks run in parallel for all services +- Services marked `status="maintenance"` skip the health check +- Set `check-health="false"` to disable checking for specific services +- No server-side component needed - runs entirely in the browser ## Icon Management diff --git a/index.html b/index.html index 3f7d0e8..84978e6 100644 --- a/index.html +++ b/index.html @@ -54,7 +54,8 @@ const port = s.getAttribute('port') || ''; const logo = s.getAttribute('logo') || ''; const hostAttr = s.getAttribute('host'); // optional public hostname or full URL - const status = s.getAttribute('status'); // optional: 'online', 'offline', 'maintenance' + const manualStatus = s.getAttribute('status'); // optional: 'online', 'offline', 'maintenance' + const checkHealth = s.getAttribute('check-health') !== 'false'; // default true, set to false to disable // Build href: prefer explicit host attribute when present. // Rules: @@ -102,11 +103,54 @@ a.appendChild(img); a.appendChild(span); - // Add status indicator if status attribute is set - if(status){ - const statusDot = document.createElement('span'); - statusDot.className = `status-dot status-${status}`; - statusDot.title = `Status: ${status}`; + // Add status indicator - will be updated by health check + const statusDot = document.createElement('span'); + statusDot.className = 'status-dot status-checking'; + statusDot.title = 'Checking status...'; + let currentStatus = 'checking'; + + // If maintenance mode is set manually, use that and skip health check + if(manualStatus === 'maintenance'){ + statusDot.className = 'status-dot status-maintenance'; + statusDot.title = 'Status: maintenance'; + currentStatus = 'maintenance'; + a.appendChild(statusDot); + } else if(checkHealth && href){ + // Show checking indicator initially + a.appendChild(statusDot); + + // Perform health check + (async function performHealthCheck(){ + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout + + const response = await fetch(href, { + method: 'HEAD', + mode: 'no-cors', // Allow cross-origin requests + cache: 'no-cache', + signal: controller.signal + }); + + clearTimeout(timeoutId); + + // In no-cors mode, opaque responses mean the server responded + // We consider this as "online" + currentStatus = 'online'; + statusDot.className = 'status-dot status-online'; + statusDot.title = 'Status: online'; + } catch(err) { + // Connection failed or timed out + currentStatus = 'offline'; + statusDot.className = 'status-dot status-offline'; + statusDot.title = 'Status: offline'; + } + })(); + } else if(manualStatus){ + // Use manual status if health check is disabled + statusDot.className = `status-dot status-${manualStatus}`; + statusDot.title = `Status: ${manualStatus}`; + currentStatus = manualStatus; a.appendChild(statusDot); } diff --git a/services.xml b/services.xml index 56db62c..118c4ac 100644 --- a/services.xml +++ b/services.xml @@ -8,16 +8,25 @@ - port: port number (optional, shows info button if present with host) - host: custom hostname or full URL (optional) - logo: filename in /logos/ (optional, default: default.svg) - - status: online, offline, or maintenance (optional, shows colored dot indicator) + - status: maintenance only (optional) - will override health check + - check-health: true/false (optional, default: true) - disable auto health check + + Status Behavior: + - If status="maintenance": Shows orange dot, skips health check + - Otherwise: Automatically pings service URL and shows: + * Gray spinning dot while checking + * Green pulsing dot if online + * Red dot if offline/unreachable + - Set check-health="false" to disable automatic checking --> - - - - - + + + + + - + diff --git a/styles.css b/styles.css index 5c1659b..0695ea0 100644 --- a/styles.css +++ b/styles.css @@ -27,7 +27,9 @@ main{max-width:1100px;margin:18px auto;padding:12px} .status-dot.status-online{background:#10b981;box-shadow:0 0 8px rgba(16,185,129,0.6)} .status-dot.status-offline{background:#ef4444;box-shadow:0 0 8px rgba(239,68,68,0.6);animation:none} .status-dot.status-maintenance{background:#f59e0b;box-shadow:0 0 8px rgba(245,158,11,0.6)} +.status-dot.status-checking{background:#6b7280;box-shadow:0 0 8px rgba(107,114,128,0.6);animation:spin 1s linear infinite} @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.6}} +@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} .info-btn{position:absolute;bottom:6px;right:6px;width:20px;height:20px;border-radius:50%;background:rgba(79,70,229,0.3);border:1px solid rgba(255,255,255,0.3);color:#fff;font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;padding:0;line-height:1;backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px)} .info-btn:hover{background:rgba(79,70,229,0.6);border-color:rgba(255,255,255,0.6);transform:scale(1.1)} .notes{margin-top:18px;background:rgba(255,255,255,0.02);padding:12px;border-radius:8px;color:var(--muted)}