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)}