diff --git a/backend/health-proxy.py b/backend/health-proxy.py index 0827e42..fd54fd7 100644 --- a/backend/health-proxy.py +++ b/backend/health-proxy.py @@ -1,6 +1,8 @@ from flask import Flask, request, jsonify, abort import requests import os +import xml.etree.ElementTree as ET +import time app = Flask(__name__) @@ -13,11 +15,50 @@ def healthcheck(): if HEALTH_TOKEN and token != HEALTH_TOKEN: return jsonify({'error': 'Unauthorized'}), 401 + # New behavior: prefer service id lookup + service_id = request.args.get('id') host = request.args.get('host') port = request.args.get('port') proto = request.args.get('proto', 'http') + + if service_id: + # Load services.xml and find service by id + try: + tree = ET.parse('/usr/share/nginx/html/services.xml') + root = tree.getroot() + except Exception as e: + return jsonify({'error': 'unable to parse services.xml', 'detail': str(e)}), 500 + + # Find service element by id + svc = None + for s in root.findall('.//service'): + sid = s.get('id') or s.get('name') + if sid and sid == service_id: + svc = s + break + if svc is None: + return jsonify({'error': 'service id not found'}), 404 + + # Determine host/port/proto preference: local-ip > tailscale-ip root > host attr > fallback to provided host + local_ip = svc.get('local-ip') + root_tailscale = root.get('tailscale-ip') + host_attr = svc.get('host') + proto_attr = svc.get('proto') or proto + port_attr = svc.get('port') or port + + # prefer local-ip if present + target_host = local_ip or root_tailscale or (host_attr.split(':')[0] if host_attr and ':' in host_attr else None) or host + target_port = port_attr or (host_attr.split(':')[1] if host_attr and ':' in host_attr else None) + target_proto = proto_attr + if not target_host or not target_port: + return jsonify({'error': 'service missing host or port mapping'}), 400 + + host = target_host + port = target_port + proto = target_proto + if not host or not port: - return jsonify({'error': 'missing parameters, expected host and port'}), 400 + return jsonify({'error': 'missing parameters, expected host and port or id'}), 400 url = f"{proto}://{host}:{port}" diff --git a/js/services-loader.js b/js/services-loader.js index 5a0094d..ab67029 100644 --- a/js/services-loader.js +++ b/js/services-loader.js @@ -82,6 +82,7 @@ // Function to create a service card function createServiceCard(s, host, allServices, tailscaleIP) { const name = s.getAttribute('name') || s.getAttribute('id') || 'unknown'; + const serviceId = s.getAttribute('id') || name.toLowerCase().replace(/\s+/g, '-'); const proto = s.getAttribute('proto') || 'http'; const port = s.getAttribute('port') || ''; const logo = s.getAttribute('logo') || ''; @@ -122,9 +123,7 @@ } } // 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)}`; + healthCheckUrl = `/healthcheck?id=${encodeURIComponent(serviceId)}`; } else { // Local service - use Tailscale IP if configured, otherwise current hostname const targetHost = localIpAttr || tailscaleIP || host; @@ -141,7 +140,7 @@ } else if(tailscaleIP){ console.log(`Service ${name}: using tailscale-ip ${tailscaleIP} for proxied health checks`); } - healthCheckUrl = `/healthcheck?host=${encodedHost}&port=${encodeURIComponent(hcPort)}&proto=${encodeURIComponent(desiredProto)}`; + healthCheckUrl = `/healthcheck?id=${encodeURIComponent(serviceId)}`; // 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.`);