Add automatic health checks and status indicators for services

This commit is contained in:
MayaChat
2025-11-24 00:03:14 -05:00
parent f14955e534
commit 5c798c075d
4 changed files with 92 additions and 24 deletions

View File

@@ -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

View File

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

View File

@@ -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" />

View File

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