Add health proxy service and Nginx configuration for health checks

This commit is contained in:
MayaChat
2025-11-24 12:26:37 -05:00
parent aa1a38b6fb
commit af2658bfce
4 changed files with 145 additions and 10 deletions

View File

@@ -82,39 +82,64 @@
// Function to create a service card
function createServiceCard(s, host, allServices, tailscaleIP) {
const name = s.getAttribute('name') || s.getAttribute('id') || 'unknown';
const proto = s.getAttribute('proto') || 'http';
const proto = s.getAttribute('proto') || 'http';
const port = s.getAttribute('port') || '';
const logo = s.getAttribute('logo') || '';
const hostAttr = s.getAttribute('host'); // optional public hostname or full URL
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.
// Build href: prefer explicit host attribute when present.
let href = '';
let healthCheckUrl = ''; // Separate URL for health checks
const pageIsSecure = window.location.protocol === 'https:';
const desiredProto = pageIsSecure ? 'https' : proto; // use https for health checks when page is secure to avoid mixed content
if(hostAttr){
// Service has a public hostname or URL
// Parse hostAttr to extract hostname and optional port
let parsedHost = hostAttr;
let parsedPort = port;
let parsedProto = proto;
if(/^https?:\/\//i.test(hostAttr)){
// remove protocol
const u = new URL(hostAttr);
parsedHost = u.hostname;
parsedPort = u.port || parsedPort;
parsedProto = u.protocol.replace(':','') || parsedProto;
href = hostAttr;
healthCheckUrl = hostAttr;
} else {
const hasPortInHost = /:\d+$/.test(hostAttr);
if(hasPortInHost){
// host:port
href = `${proto}://${hostAttr}`;
healthCheckUrl = `${proto}://${hostAttr}`;
parsedHost = hostAttr.split(':')[0];
parsedPort = hostAttr.split(':')[1];
} else {
href = `https://${hostAttr}`;
healthCheckUrl = `https://${hostAttr}`;
parsedHost = hostAttr;
}
}
// Always proxy health checks via same-origin endpoint to avoid mixed-content
const encodedHost = encodeURIComponent(parsedHost);
const hcPort = parsedPort || (pageIsSecure ? '443' : (parsedProto === 'https' ? '443' : '80'));
healthCheckUrl = `/healthcheck?host=${encodedHost}&port=${encodeURIComponent(hcPort)}&proto=${encodeURIComponent(parsedProto)}`;
} else {
// Local service - use Tailscale IP if configured, otherwise current hostname
const targetHost = tailscaleIP || host;
let portPart = '';
if(port && !((proto==='http'&&port==='80')||(proto==='https'&&port==='443'))){ portPart = ':'+port; }
href = `${proto}://${targetHost}${portPart}`;
healthCheckUrl = `${proto}://${targetHost}${portPart}`;
// Keep the link protocol as the defined proto (so clicking uses the intended protocol),
// but use the server-side health proxy for health checks to avoid mixed content blocking.
href = `${proto}://${targetHost}${portPart}`;
// Use our same-origin proxy path which nginx will forward to the health-proxy service.
const encodedHost = encodeURIComponent(targetHost);
const hcPort = port || (desiredProto === 'https' ? '443' : '80');
healthCheckUrl = `/healthcheck?host=${encodedHost}&port=${encodeURIComponent(hcPort)}&proto=${encodeURIComponent(desiredProto)}`;
// Warn when site is secure but service link is HTTP and target is a private IP
if(pageIsSecure && proto === 'http' && targetHost === tailscaleIP){
console.warn(`Service ${name} will use HTTP at ${href}, but the page is loaded over HTTPS — mixed-content checks will prevent programmatic health checks to the service. Consider enabling TLS or providing a public hostname.`);
}
}
const a = document.createElement('a');
@@ -136,23 +161,26 @@
a.appendChild(img);
a.appendChild(span);
// Add status indicator - will be updated by health check
// 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 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 && healthCheckUrl){
} else if(checkHealth && healthCheckUrl){
// Show checking indicator initially
a.appendChild(statusDot);
// Perform health check using healthCheckUrl (Tailscale IP for local services)
if(healthCheckUrl !== href){
console.log(`Service ${name}: using healthCheckUrl (${healthCheckUrl}) while link is ${href}`);
}
(async function performHealthCheck(){
try {
const controller = new AbortController();
@@ -173,6 +201,22 @@
statusDot.title = 'Status: online';
} catch(err) {
// Connection failed or timed out
// If the page is not secure, try fallback to http if we used https for health check
if(!pageIsSecure && healthCheckUrl.startsWith('https:')){
try {
const fallback = healthCheckUrl.replace(/^https:/i, 'http:');
const controller2 = new AbortController();
const timeoutId2 = setTimeout(() => controller2.abort(), 5000);
await fetch(fallback, { method: 'HEAD', mode: 'no-cors', cache: 'no-cache', signal: controller2.signal });
clearTimeout(timeoutId2);
currentStatus = 'online';
statusDot.className = 'status-dot status-online';
statusDot.title = 'Status: online';
return;
} catch(fErr){
// continue to mark offline
}
}
currentStatus = 'offline';
statusDot.className = 'status-dot status-offline';
statusDot.title = 'Status: offline';