Enhance service card creation with Tailscale IP support and update services.xml to include tailscale-ip configuration

This commit is contained in:
MayaChat
2025-11-24 12:01:00 -05:00
parent 3b09920eee
commit 242d046d1f
2 changed files with 25 additions and 8 deletions

View File

@@ -18,6 +18,11 @@
throw new Error('XML parsing error: ' + parseError.textContent); throw new Error('XML parsing error: ' + parseError.textContent);
} }
// Get Tailscale IP from root services element
const servicesRoot = doc.getElementsByTagName('services')[0];
const tailscaleIP = servicesRoot ? servicesRoot.getAttribute('tailscale-ip') : null;
console.log('Tailscale IP:', tailscaleIP || 'not configured');
// Check if we have groups or just services // Check if we have groups or just services
const groups = Array.from(doc.getElementsByTagName('group')); const groups = Array.from(doc.getElementsByTagName('group'));
const hasGroups = groups.length > 0; const hasGroups = groups.length > 0;
@@ -47,7 +52,7 @@
grid.className = 'grid'; grid.className = 'grid';
services.forEach(s => { services.forEach(s => {
const card = createServiceCard(s, host, allServices); const card = createServiceCard(s, host, allServices, tailscaleIP);
grid.appendChild(card); grid.appendChild(card);
}); });
@@ -67,7 +72,7 @@
grid.id = 'services-grid'; grid.id = 'services-grid';
services.forEach(s => { services.forEach(s => {
const card = createServiceCard(s, host, allServices); const card = createServiceCard(s, host, allServices, tailscaleIP);
grid.appendChild(card); grid.appendChild(card);
}); });
@@ -75,7 +80,7 @@
} }
// Function to create a service card // Function to create a service card
function createServiceCard(s, host, allServices) { function createServiceCard(s, host, allServices, tailscaleIP) {
const name = s.getAttribute('name') || s.getAttribute('id') || 'unknown'; 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 port = s.getAttribute('port') || '';
@@ -86,21 +91,30 @@
// Build href: prefer explicit host attribute when present. // Build href: prefer explicit host attribute when present.
let href = ''; let href = '';
let healthCheckUrl = ''; // Separate URL for health checks
if(hostAttr){ if(hostAttr){
// Service has a public hostname or URL
if(/^https?:\/\//i.test(hostAttr)){ if(/^https?:\/\//i.test(hostAttr)){
href = hostAttr; href = hostAttr;
healthCheckUrl = hostAttr;
} else { } else {
const hasPortInHost = /:\d+$/.test(hostAttr); const hasPortInHost = /:\d+$/.test(hostAttr);
if(hasPortInHost){ if(hasPortInHost){
href = `${proto}://${hostAttr}`; href = `${proto}://${hostAttr}`;
healthCheckUrl = `${proto}://${hostAttr}`;
} else { } else {
href = `https://${hostAttr}`; href = `https://${hostAttr}`;
healthCheckUrl = `https://${hostAttr}`;
} }
} }
} else { } else {
// Local service - use Tailscale IP if configured, otherwise current hostname
const targetHost = tailscaleIP || host;
let portPart = ''; let portPart = '';
if(port && !((proto==='http'&&port==='80')||(proto==='https'&&port==='443'))){ portPart = ':'+port; } if(port && !((proto==='http'&&port==='80')||(proto==='https'&&port==='443'))){ portPart = ':'+port; }
href = `${proto}://${host}${portPart}`; href = `${proto}://${targetHost}${portPart}`;
healthCheckUrl = `${proto}://${targetHost}${portPart}`;
} }
const a = document.createElement('a'); const a = document.createElement('a');
@@ -134,17 +148,17 @@
statusDot.title = 'Status: maintenance'; statusDot.title = 'Status: maintenance';
currentStatus = 'maintenance'; currentStatus = 'maintenance';
a.appendChild(statusDot); a.appendChild(statusDot);
} else if(checkHealth && href){ } else if(checkHealth && healthCheckUrl){
// Show checking indicator initially // Show checking indicator initially
a.appendChild(statusDot); a.appendChild(statusDot);
// Perform health check // Perform health check using healthCheckUrl (Tailscale IP for local services)
(async function performHealthCheck(){ (async function performHealthCheck(){
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
const response = await fetch(href, { const response = await fetch(healthCheckUrl, {
method: 'HEAD', method: 'HEAD',
mode: 'no-cors', // Allow cross-origin requests mode: 'no-cors', // Allow cross-origin requests
cache: 'no-cache', cache: 'no-cache',

View File

@@ -2,6 +2,9 @@
<!-- <!--
services.xml - simple list of services for the homepage services.xml - simple list of services for the homepage
Configuration:
- tailscale-ip: IP address to use for local services (optional, default: auto-detect)
Structure: Structure:
- Use <group> elements to organize services into categories - Use <group> elements to organize services into categories
- Each group has a 'name' attribute for the category title - Each group has a 'name' attribute for the category title
@@ -25,7 +28,7 @@
* Red dot if offline/unreachable * Red dot if offline/unreachable
- Set check-health="false" to disable automatic checking - Set check-health="false" to disable automatic checking
--> -->
<services> <services tailscale-ip="100.124.17.41">
<group name="Management"> <group name="Management">
<service id="portainer" name="Portainer" proto="https" port="9443" logo="portainer.svg" /> <service id="portainer" name="Portainer" proto="https" port="9443" logo="portainer.svg" />
<service id="uptime-kuma" name="Uptime Kuma" proto="http" port="3001" logo="uptime-kuma.svg" /> <service id="uptime-kuma" name="Uptime Kuma" proto="http" port="3001" logo="uptime-kuma.svg" />