Add automatic health checks and status indicators for services
This commit is contained in:
35
README.md
35
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
|
||||
<!-- Green pulsing dot for online services -->
|
||||
<service name="Jellyfin" status="online" proto="http" port="8096" logo="jellyfin.svg" />
|
||||
<!-- Automatic health check (default behavior) -->
|
||||
<service name="Jellyfin" proto="http" port="8096" logo="jellyfin.svg" />
|
||||
|
||||
<!-- Red dot for offline services -->
|
||||
<service name="Legacy App" status="offline" proto="http" port="8080" logo="app.svg" />
|
||||
|
||||
<!-- Orange dot for services under maintenance -->
|
||||
<!-- Manual maintenance mode (skips health check) -->
|
||||
<service name="Nextcloud" status="maintenance" host="cloud.example.com" logo="nextcloud.svg" />
|
||||
|
||||
<!-- Disable health check for a service -->
|
||||
<service name="Legacy App" check-health="false" proto="http" port="8080" logo="app.svg" />
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
||||
56
index.html
56
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);
|
||||
}
|
||||
|
||||
|
||||
23
services.xml
23
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
|
||||
-->
|
||||
<services>
|
||||
<service id="portainer" name="Portainer" proto="https" port="9443" logo="portainer.svg" status="online" />
|
||||
<service id="portainer-http" name="Portainer (HTTP)" proto="http" port="8000" logo="portainer.svg" status="offline" />
|
||||
<service id="homeassistant" name="Home Assistant" proto="http" port="8123" logo="homeassistant.svg" status="online" />
|
||||
<service id="jellyfin" name="Jellyfin" proto="http" port="8096" logo="jellyfin.svg" host="jellyfin.spatulaa.com" status="online" />
|
||||
<service id="nextcloud" name="Nextcloud" proto="http" port="8080" logo="nextcloud.svg" host="cloud.spatulaa.com" status="online" />
|
||||
<service id="portainer" name="Portainer" proto="https" port="9443" logo="portainer.svg" />
|
||||
<service id="portainer-http" name="Portainer (HTTP)" proto="http" port="8000" logo="portainer.svg" />
|
||||
<service id="homeassistant" name="Home Assistant" proto="http" port="8123" logo="homeassistant.svg" />
|
||||
<service id="jellyfin" name="Jellyfin" proto="http" port="8096" logo="jellyfin.svg" host="jellyfin.spatulaa.com" />
|
||||
<service id="nextcloud" name="Nextcloud" proto="http" port="8080" logo="nextcloud.svg" host="cloud.spatulaa.com" />
|
||||
<service id="filebrowser" name="FileBrowser" proto="http" port="8986" logo="filebrowser.svg" />
|
||||
<service id="gitea" name="Gitea" proto="http" port="3000" logo="gitea.svg" host="git.spatulaa.com" status="online" />
|
||||
<service id="gitea" name="Gitea" proto="http" port="3000" logo="gitea.svg" host="git.spatulaa.com" />
|
||||
<service id="uptime-kuma" name="Uptime Kuma" proto="http" port="3001" logo="uptime-kuma.svg" />
|
||||
<service id="picoshare" name="Picoshare" proto="http" port="4001" logo="picoshare.svg" />
|
||||
<service id="transmission" name="Transmission" proto="http" port="9091" logo="transmission.svg" />
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user