feat(service-registry): Add dynamic health checks and URL readiness wizard
- Add health check RPCD methods: - check_service_health: Check DNS, cert, firewall for single domain - check_all_health: Batch check all published services - Add URL Readiness Checker wizard card to dashboard: - Check if domain DNS resolves correctly - Verify firewall ports 80/443 are open - Check SSL certificate status - Show actionable recommendations - Display inline health status badges on service rows: - DNS resolution status (ok/failed) - Certificate expiry (ok/warning/critical/expired) - Add health summary bar showing overall system status - Add per-service health check button Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b762bffa44
commit
ab8e0c44bc
@ -87,6 +87,19 @@ var callSaveLandingConfig = rpc.declare({
|
|||||||
expect: {}
|
expect: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var callCheckServiceHealth = rpc.declare({
|
||||||
|
object: 'luci.service-registry',
|
||||||
|
method: 'check_service_health',
|
||||||
|
params: ['service_id', 'domain'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callCheckAllHealth = rpc.declare({
|
||||||
|
object: 'luci.service-registry',
|
||||||
|
method: 'check_all_health',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
// HAProxy status for provider info
|
// HAProxy status for provider info
|
||||||
var callHAProxyStatus = rpc.declare({
|
var callHAProxyStatus = rpc.declare({
|
||||||
object: 'luci.haproxy',
|
object: 'luci.haproxy',
|
||||||
@ -220,5 +233,37 @@ return baseclass.extend({
|
|||||||
// Full publish with HAProxy + Tor
|
// Full publish with HAProxy + Tor
|
||||||
fullPublish: function(name, port, domain) {
|
fullPublish: function(name, port, domain) {
|
||||||
return this.publishService(name, port, domain, true, 'services', '');
|
return this.publishService(name, port, domain, true, 'services', '');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check health of a single service
|
||||||
|
checkServiceHealth: function(serviceId, domain) {
|
||||||
|
return callCheckServiceHealth(serviceId || '', domain || '');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check health of all published services
|
||||||
|
checkAllHealth: function() {
|
||||||
|
return callCheckAllHealth();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get dashboard data with health status
|
||||||
|
getDashboardDataWithHealth: function() {
|
||||||
|
return Promise.all([
|
||||||
|
callListServices().catch(function(e) { console.error('list_services failed:', e); return { services: [], providers: {} }; }),
|
||||||
|
callListCategories().catch(function(e) { console.error('list_categories failed:', e); return { categories: [] }; }),
|
||||||
|
callGetLandingConfig().catch(function(e) { console.error('get_landing_config failed:', e); return {}; }),
|
||||||
|
callHAProxyStatus().catch(function() { return { enabled: false }; }),
|
||||||
|
callTorStatus().catch(function() { return { enabled: false }; }),
|
||||||
|
callCheckAllHealth().catch(function(e) { console.error('check_all_health failed:', e); return { health: {} }; })
|
||||||
|
]).then(function(results) {
|
||||||
|
return {
|
||||||
|
services: results[0].services || [],
|
||||||
|
providers: results[0].providers || {},
|
||||||
|
categories: results[1].categories || [],
|
||||||
|
landing: results[2],
|
||||||
|
haproxy: results[3],
|
||||||
|
tor: results[4],
|
||||||
|
health: results[5].health || {}
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -12,6 +12,13 @@ var catIcons = {
|
|||||||
'monitoring': '📊', 'other': '🔗'
|
'monitoring': '📊', 'other': '🔗'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Health status icons
|
||||||
|
var healthIcons = {
|
||||||
|
'dns': { 'ok': '🌐', 'failed': '❌', 'none': '⚪' },
|
||||||
|
'cert': { 'ok': '🔒', 'warning': '⚠️', 'critical': '🔴', 'expired': '💀', 'missing': '⚪', 'none': '⚪' },
|
||||||
|
'firewall': { 'ok': '✅', 'partial': '⚠️', 'closed': '🚫' }
|
||||||
|
};
|
||||||
|
|
||||||
// Generate QR code using QR Server API (free, reliable)
|
// Generate QR code using QR Server API (free, reliable)
|
||||||
function generateQRCodeImg(data, size) {
|
function generateQRCodeImg(data, size) {
|
||||||
var url = 'https://api.qrserver.com/v1/create-qr-code/?size=' + size + 'x' + size + '&data=' + encodeURIComponent(data);
|
var url = 'https://api.qrserver.com/v1/create-qr-code/?size=' + size + 'x' + size + '&data=' + encodeURIComponent(data);
|
||||||
@ -21,9 +28,10 @@ function generateQRCodeImg(data, size) {
|
|||||||
return view.extend({
|
return view.extend({
|
||||||
title: _('Service Registry'),
|
title: _('Service Registry'),
|
||||||
pollInterval: 30,
|
pollInterval: 30,
|
||||||
|
healthData: null,
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
return api.getDashboardData();
|
return api.getDashboardDataWithHealth();
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
@ -31,6 +39,9 @@ return view.extend({
|
|||||||
var services = data.services || [];
|
var services = data.services || [];
|
||||||
var providers = data.providers || {};
|
var providers = data.providers || {};
|
||||||
|
|
||||||
|
// Store health data for service lookups
|
||||||
|
this.healthData = data.health || {};
|
||||||
|
|
||||||
// Load CSS
|
// Load CSS
|
||||||
var style = document.createElement('style');
|
var style = document.createElement('style');
|
||||||
style.textContent = this.getStyles();
|
style.textContent = this.getStyles();
|
||||||
@ -41,12 +52,177 @@ return view.extend({
|
|||||||
|
|
||||||
return E('div', { 'class': 'sr-compact' }, [
|
return E('div', { 'class': 'sr-compact' }, [
|
||||||
this.renderHeader(services, providers, data.haproxy, data.tor),
|
this.renderHeader(services, providers, data.haproxy, data.tor),
|
||||||
|
this.renderHealthSummary(data.health),
|
||||||
|
this.renderUrlChecker(),
|
||||||
this.renderSection('📡 Published Services', published, true),
|
this.renderSection('📡 Published Services', published, true),
|
||||||
this.renderSection('🔍 Discovered Services', unpublished, false),
|
this.renderSection('🔍 Discovered Services', unpublished, false),
|
||||||
this.renderLandingLink(data.landing)
|
this.renderLandingLink(data.landing)
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderHealthSummary: function(health) {
|
||||||
|
if (!health || !health.firewall) return E('div');
|
||||||
|
|
||||||
|
var firewallStatus = health.firewall.status || 'unknown';
|
||||||
|
var firewallIcon = healthIcons.firewall[firewallStatus] || '❓';
|
||||||
|
var haproxyStatus = health.haproxy && health.haproxy.status === 'running' ? '🟢' : '🔴';
|
||||||
|
var torStatus = health.tor && health.tor.status === 'running' ? '🟢' : '🔴';
|
||||||
|
|
||||||
|
// Count service health
|
||||||
|
var services = health.services || [];
|
||||||
|
var dnsOk = services.filter(function(s) { return s.dns_status === 'ok'; }).length;
|
||||||
|
var certOk = services.filter(function(s) { return s.cert_status === 'ok'; }).length;
|
||||||
|
var certWarn = services.filter(function(s) { return s.cert_status === 'warning' || s.cert_status === 'critical'; }).length;
|
||||||
|
|
||||||
|
return E('div', { 'class': 'sr-health-bar' }, [
|
||||||
|
E('span', { 'class': 'sr-health-item', 'title': 'Firewall ports 80/443' },
|
||||||
|
firewallIcon + ' Firewall: ' + firewallStatus),
|
||||||
|
E('span', { 'class': 'sr-health-item', 'title': 'HAProxy container' },
|
||||||
|
haproxyStatus + ' HAProxy'),
|
||||||
|
E('span', { 'class': 'sr-health-item', 'title': 'Tor daemon' },
|
||||||
|
torStatus + ' Tor'),
|
||||||
|
services.length > 0 ? E('span', { 'class': 'sr-health-item' },
|
||||||
|
'🌐 DNS: ' + dnsOk + '/' + services.length) : null,
|
||||||
|
services.length > 0 ? E('span', { 'class': 'sr-health-item' },
|
||||||
|
'🔒 Certs: ' + certOk + '/' + services.length +
|
||||||
|
(certWarn > 0 ? ' (⚠️ ' + certWarn + ')' : '')) : null
|
||||||
|
].filter(Boolean));
|
||||||
|
},
|
||||||
|
|
||||||
|
renderUrlChecker: function() {
|
||||||
|
var self = this;
|
||||||
|
return E('div', { 'class': 'sr-wizard-card' }, [
|
||||||
|
E('div', { 'class': 'sr-wizard-header' }, [
|
||||||
|
E('span', { 'class': 'sr-wizard-icon' }, '🔍'),
|
||||||
|
E('span', { 'class': 'sr-wizard-title' }, 'URL Readiness Checker'),
|
||||||
|
E('span', { 'class': 'sr-wizard-desc' }, 'Check if a domain is ready to be hosted')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'sr-wizard-form' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'id': 'url-check-domain',
|
||||||
|
'placeholder': 'Enter domain (e.g., example.com)',
|
||||||
|
'class': 'sr-wizard-input'
|
||||||
|
}),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-action',
|
||||||
|
'click': ui.createHandlerFn(this, 'handleUrlCheck')
|
||||||
|
}, '🔍 Check')
|
||||||
|
]),
|
||||||
|
E('div', { 'id': 'url-check-results', 'class': 'sr-check-results' })
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUrlCheck: function() {
|
||||||
|
var self = this;
|
||||||
|
var domain = document.getElementById('url-check-domain').value.trim();
|
||||||
|
var resultsDiv = document.getElementById('url-check-results');
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
resultsDiv.innerHTML = '<div class="sr-check-error">Please enter a domain</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean domain (remove protocol if present)
|
||||||
|
domain = domain.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = '<div class="sr-check-loading">🔄 Checking ' + domain + '...</div>';
|
||||||
|
|
||||||
|
api.checkServiceHealth('', domain).then(function(result) {
|
||||||
|
if (!result.success) {
|
||||||
|
resultsDiv.innerHTML = '<div class="sr-check-error">❌ Check failed: ' + (result.error || 'Unknown error') + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '<div class="sr-check-grid">';
|
||||||
|
|
||||||
|
// DNS Status
|
||||||
|
var dnsStatus = result.dns || {};
|
||||||
|
var dnsIcon = healthIcons.dns[dnsStatus.status] || '❓';
|
||||||
|
var dnsClass = dnsStatus.status === 'ok' ? 'sr-check-ok' : 'sr-check-fail';
|
||||||
|
html += '<div class="sr-check-item ' + dnsClass + '">';
|
||||||
|
html += '<span class="sr-check-icon">' + dnsIcon + '</span>';
|
||||||
|
html += '<span class="sr-check-label">DNS Resolution</span>';
|
||||||
|
if (dnsStatus.status === 'ok') {
|
||||||
|
html += '<span class="sr-check-value">✅ Resolves to ' + dnsStatus.resolved_ip + '</span>';
|
||||||
|
} else {
|
||||||
|
html += '<span class="sr-check-value">❌ DNS not configured or not resolving</span>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Firewall Status
|
||||||
|
var fwStatus = result.firewall || {};
|
||||||
|
var fwIcon = healthIcons.firewall[fwStatus.status] || '❓';
|
||||||
|
var fwClass = fwStatus.status === 'ok' ? 'sr-check-ok' : (fwStatus.status === 'partial' ? 'sr-check-warn' : 'sr-check-fail');
|
||||||
|
html += '<div class="sr-check-item ' + fwClass + '">';
|
||||||
|
html += '<span class="sr-check-icon">' + fwIcon + '</span>';
|
||||||
|
html += '<span class="sr-check-label">Firewall Ports</span>';
|
||||||
|
var ports = [];
|
||||||
|
if (fwStatus.http_open) ports.push('80');
|
||||||
|
if (fwStatus.https_open) ports.push('443');
|
||||||
|
html += '<span class="sr-check-value">' + (ports.length ? 'Open: ' + ports.join(', ') : '❌ Ports 80/443 not open') + '</span>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Certificate Status
|
||||||
|
var certStatus = result.certificate || {};
|
||||||
|
var certIcon = healthIcons.cert[certStatus.status] || '❓';
|
||||||
|
var certClass = certStatus.status === 'ok' ? 'sr-check-ok' : (certStatus.status === 'warning' ? 'sr-check-warn' : 'sr-check-fail');
|
||||||
|
html += '<div class="sr-check-item ' + certClass + '">';
|
||||||
|
html += '<span class="sr-check-icon">' + certIcon + '</span>';
|
||||||
|
html += '<span class="sr-check-label">SSL Certificate</span>';
|
||||||
|
if (certStatus.status === 'ok' || certStatus.status === 'warning') {
|
||||||
|
html += '<span class="sr-check-value">' + certStatus.days_left + ' days remaining</span>';
|
||||||
|
} else if (certStatus.status === 'expired') {
|
||||||
|
html += '<span class="sr-check-value">❌ Certificate expired</span>';
|
||||||
|
} else if (certStatus.status === 'missing') {
|
||||||
|
html += '<span class="sr-check-value">⚪ No certificate (request via HAProxy)</span>';
|
||||||
|
} else {
|
||||||
|
html += '<span class="sr-check-value">⚪ Not applicable</span>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// HAProxy Status
|
||||||
|
var haStatus = result.haproxy || {};
|
||||||
|
var haIcon = haStatus.status === 'running' ? '🟢' : '🔴';
|
||||||
|
var haClass = haStatus.status === 'running' ? 'sr-check-ok' : 'sr-check-fail';
|
||||||
|
html += '<div class="sr-check-item ' + haClass + '">';
|
||||||
|
html += '<span class="sr-check-icon">' + haIcon + '</span>';
|
||||||
|
html += '<span class="sr-check-label">HAProxy</span>';
|
||||||
|
html += '<span class="sr-check-value">' + (haStatus.status === 'running' ? '✅ Running' : '❌ Not running') + '</span>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Summary and recommendation
|
||||||
|
var allOk = dnsStatus.status === 'ok' && fwStatus.status === 'ok' && haStatus.status === 'running';
|
||||||
|
var needsCert = certStatus.status === 'missing';
|
||||||
|
|
||||||
|
html += '<div class="sr-check-summary">';
|
||||||
|
if (allOk && !needsCert) {
|
||||||
|
html += '<div class="sr-check-ready">✅ ' + domain + ' is ready and serving!</div>';
|
||||||
|
} else if (allOk && needsCert) {
|
||||||
|
html += '<div class="sr-check-almost">⚠️ ' + domain + ' is ready - just need SSL certificate</div>';
|
||||||
|
html += '<a href="/cgi-bin/luci/admin/services/haproxy/certificates" class="sr-check-action">📜 Request Certificate</a>';
|
||||||
|
} else {
|
||||||
|
html += '<div class="sr-check-notready">❌ ' + domain + ' needs configuration</div>';
|
||||||
|
if (dnsStatus.status !== 'ok') {
|
||||||
|
html += '<div class="sr-check-tip">💡 Point DNS A record to your public IP</div>';
|
||||||
|
}
|
||||||
|
if (fwStatus.status !== 'ok') {
|
||||||
|
html += '<div class="sr-check-tip">💡 Open ports 80 and 443 in firewall</div>';
|
||||||
|
}
|
||||||
|
if (haStatus.status !== 'running') {
|
||||||
|
html += '<div class="sr-check-tip">💡 Start HAProxy container</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = html;
|
||||||
|
}).catch(function(err) {
|
||||||
|
resultsDiv.innerHTML = '<div class="sr-check-error">❌ Error: ' + err.message + '</div>';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
renderHeader: function(services, providers, haproxy, tor) {
|
renderHeader: function(services, providers, haproxy, tor) {
|
||||||
var published = services.filter(function(s) { return s.published; }).length;
|
var published = services.filter(function(s) { return s.published; }).length;
|
||||||
var running = services.filter(function(s) { return s.status === 'running'; }).length;
|
var running = services.filter(function(s) { return s.status === 'running'; }).length;
|
||||||
@ -135,20 +311,46 @@ return view.extend({
|
|||||||
portDisplay = ':' + service.haproxy.backend_port;
|
portDisplay = ':' + service.haproxy.backend_port;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSL/Cert badge
|
// Get health status for this domain (if available)
|
||||||
var sslBadge = null;
|
var healthBadges = [];
|
||||||
if (service.haproxy) {
|
var domain = service.haproxy && service.haproxy.domain;
|
||||||
|
if (domain && this.healthData && this.healthData.services) {
|
||||||
|
var svcHealth = this.healthData.services.find(function(h) {
|
||||||
|
return h.domain === domain;
|
||||||
|
});
|
||||||
|
if (svcHealth) {
|
||||||
|
// DNS badge
|
||||||
|
var dnsIcon = healthIcons.dns[svcHealth.dns_status] || '❓';
|
||||||
|
var dnsTitle = svcHealth.dns_status === 'ok' ?
|
||||||
|
'DNS OK: ' + svcHealth.dns_ip : 'DNS: ' + svcHealth.dns_status;
|
||||||
|
healthBadges.push(E('span', {
|
||||||
|
'class': 'sr-badge sr-badge-dns sr-badge-' + svcHealth.dns_status,
|
||||||
|
'title': dnsTitle
|
||||||
|
}, dnsIcon));
|
||||||
|
|
||||||
|
// Cert badge
|
||||||
|
var certIcon = healthIcons.cert[svcHealth.cert_status] || '❓';
|
||||||
|
var certTitle = svcHealth.cert_status === 'ok' || svcHealth.cert_status === 'warning' ?
|
||||||
|
'Cert: ' + svcHealth.cert_days + ' days' : 'Cert: ' + svcHealth.cert_status;
|
||||||
|
healthBadges.push(E('span', {
|
||||||
|
'class': 'sr-badge sr-badge-cert sr-badge-' + svcHealth.cert_status,
|
||||||
|
'title': certTitle
|
||||||
|
}, certIcon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSL/Cert badge (fallback if no health data)
|
||||||
|
if (healthBadges.length === 0 && service.haproxy) {
|
||||||
if (service.haproxy.acme) {
|
if (service.haproxy.acme) {
|
||||||
sslBadge = E('span', { 'class': 'sr-badge sr-badge-acme', 'title': 'ACME Certificate' }, '🔒');
|
healthBadges.push(E('span', { 'class': 'sr-badge sr-badge-acme', 'title': 'ACME Certificate' }, '🔒'));
|
||||||
} else if (service.haproxy.ssl) {
|
} else if (service.haproxy.ssl) {
|
||||||
sslBadge = E('span', { 'class': 'sr-badge sr-badge-ssl', 'title': 'SSL Enabled' }, '🔐');
|
healthBadges.push(E('span', { 'class': 'sr-badge sr-badge-ssl', 'title': 'SSL Enabled' }, '🔐'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tor badge
|
// Tor badge
|
||||||
var torBadge = null;
|
|
||||||
if (service.tor && service.tor.enabled) {
|
if (service.tor && service.tor.enabled) {
|
||||||
torBadge = E('span', { 'class': 'sr-badge sr-badge-tor', 'title': 'Tor Hidden Service' }, '🧅');
|
healthBadges.push(E('span', { 'class': 'sr-badge sr-badge-tor', 'title': 'Tor Hidden Service' }, '🧅'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// QR button for published services with URLs
|
// QR button for published services with URLs
|
||||||
@ -161,6 +363,16 @@ return view.extend({
|
|||||||
}, '📱');
|
}, '📱');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Health check button for published services with domains
|
||||||
|
var checkBtn = null;
|
||||||
|
if (isPublished && domain) {
|
||||||
|
checkBtn = E('button', {
|
||||||
|
'class': 'sr-btn sr-btn-check',
|
||||||
|
'title': 'Check Health',
|
||||||
|
'click': ui.createHandlerFn(this, 'handleServiceHealthCheck', service)
|
||||||
|
}, '🔍');
|
||||||
|
}
|
||||||
|
|
||||||
// Action button
|
// Action button
|
||||||
var actionBtn;
|
var actionBtn;
|
||||||
if (isPublished) {
|
if (isPublished) {
|
||||||
@ -187,12 +399,24 @@ return view.extend({
|
|||||||
E('span', { 'class': 'sr-col-url' },
|
E('span', { 'class': 'sr-col-url' },
|
||||||
urlDisplay ? E('a', { 'href': urlDisplay.startsWith('http') ? urlDisplay : 'http://' + urlDisplay, 'target': '_blank' }, urlDisplay) : '-'
|
urlDisplay ? E('a', { 'href': urlDisplay.startsWith('http') ? urlDisplay : 'http://' + urlDisplay, 'target': '_blank' }, urlDisplay) : '-'
|
||||||
),
|
),
|
||||||
E('span', { 'class': 'sr-col-badges' }, [sslBadge, torBadge].filter(Boolean)),
|
E('span', { 'class': 'sr-col-badges' }, healthBadges),
|
||||||
E('span', { 'class': 'sr-col-qr' }, qrBtn),
|
E('span', { 'class': 'sr-col-qr' }, [qrBtn, checkBtn].filter(Boolean)),
|
||||||
E('span', { 'class': 'sr-col-action' }, actionBtn)
|
E('span', { 'class': 'sr-col-action' }, actionBtn)
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleServiceHealthCheck: function(service) {
|
||||||
|
var self = this;
|
||||||
|
var domain = service.haproxy && service.haproxy.domain;
|
||||||
|
if (!domain) return;
|
||||||
|
|
||||||
|
document.getElementById('url-check-domain').value = domain;
|
||||||
|
this.handleUrlCheck();
|
||||||
|
|
||||||
|
// Scroll to the checker
|
||||||
|
document.querySelector('.sr-wizard-card').scrollIntoView({ behavior: 'smooth' });
|
||||||
|
},
|
||||||
|
|
||||||
handleShowQR: function(service) {
|
handleShowQR: function(service) {
|
||||||
var urls = service.urls || {};
|
var urls = service.urls || {};
|
||||||
var qrBoxes = [];
|
var qrBoxes = [];
|
||||||
@ -321,6 +545,41 @@ return view.extend({
|
|||||||
.sr-providers-bar { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
|
.sr-providers-bar { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
|
||||||
.sr-provider-badge { background: rgba(255,255,255,0.1); padding: 4px 10px; border-radius: 12px; font-size: 0.8em; }
|
.sr-provider-badge { background: rgba(255,255,255,0.1); padding: 4px 10px; border-radius: 12px; font-size: 0.8em; }
|
||||||
|
|
||||||
|
/* Health Summary Bar */
|
||||||
|
.sr-health-bar { display: flex; gap: 15px; margin-bottom: 15px; padding: 10px 15px; background: #f0f7ff; border-radius: 6px; border-left: 4px solid #0099cc; flex-wrap: wrap; }
|
||||||
|
@media (prefers-color-scheme: dark) { .sr-health-bar { background: #1a2a3e; } }
|
||||||
|
.sr-health-item { font-size: 0.9em; }
|
||||||
|
|
||||||
|
/* URL Checker Wizard Card */
|
||||||
|
.sr-wizard-card { background: linear-gradient(135deg, #0a192f 0%, #172a45 100%); border-radius: 12px; padding: 20px; margin-bottom: 25px; color: #fff; }
|
||||||
|
.sr-wizard-header { display: flex; align-items: center; gap: 12px; margin-bottom: 15px; }
|
||||||
|
.sr-wizard-icon { font-size: 1.8em; }
|
||||||
|
.sr-wizard-title { font-size: 1.2em; font-weight: 600; }
|
||||||
|
.sr-wizard-desc { font-size: 0.85em; opacity: 0.7; margin-left: auto; }
|
||||||
|
.sr-wizard-form { display: flex; gap: 10px; align-items: center; }
|
||||||
|
.sr-wizard-input { flex: 1; padding: 10px 15px; border: 1px solid #334155; border-radius: 6px; background: #0f172a; color: #fff; font-size: 1em; }
|
||||||
|
.sr-wizard-input::placeholder { color: #64748b; }
|
||||||
|
|
||||||
|
/* Health Check Results */
|
||||||
|
.sr-check-results { margin-top: 15px; }
|
||||||
|
.sr-check-loading { text-align: center; padding: 20px; font-size: 1.1em; }
|
||||||
|
.sr-check-error { background: #450a0a; padding: 12px 15px; border-radius: 6px; color: #fca5a5; }
|
||||||
|
.sr-check-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; }
|
||||||
|
.sr-check-item { background: #0f172a; padding: 12px 15px; border-radius: 8px; display: flex; align-items: center; gap: 10px; border-left: 3px solid #334155; }
|
||||||
|
.sr-check-item.sr-check-ok { border-left-color: #22c55e; }
|
||||||
|
.sr-check-item.sr-check-warn { border-left-color: #eab308; }
|
||||||
|
.sr-check-item.sr-check-fail { border-left-color: #ef4444; }
|
||||||
|
.sr-check-icon { font-size: 1.3em; }
|
||||||
|
.sr-check-label { font-weight: 600; font-size: 0.9em; min-width: 100px; }
|
||||||
|
.sr-check-value { font-size: 0.85em; opacity: 0.8; }
|
||||||
|
.sr-check-summary { margin-top: 15px; padding: 15px; background: #0f172a; border-radius: 8px; text-align: center; }
|
||||||
|
.sr-check-ready { font-size: 1.1em; color: #22c55e; font-weight: 600; }
|
||||||
|
.sr-check-almost { font-size: 1.1em; color: #eab308; font-weight: 600; }
|
||||||
|
.sr-check-notready { font-size: 1.1em; color: #ef4444; font-weight: 600; margin-bottom: 10px; }
|
||||||
|
.sr-check-tip { font-size: 0.85em; opacity: 0.8; margin-top: 5px; }
|
||||||
|
.sr-check-action { display: inline-block; margin-top: 10px; padding: 8px 16px; background: #0099cc; color: #fff; text-decoration: none; border-radius: 6px; font-size: 0.9em; }
|
||||||
|
.sr-check-action:hover { background: #00b3e6; }
|
||||||
|
|
||||||
.sr-section { margin-bottom: 25px; }
|
.sr-section { margin-bottom: 25px; }
|
||||||
.sr-section-title { font-size: 1.1em; margin: 0 0 10px 0; padding-bottom: 8px; border-bottom: 2px solid #0ff; color: #0ff; }
|
.sr-section-title { font-size: 1.1em; margin: 0 0 10px 0; padding-bottom: 8px; border-bottom: 2px solid #0ff; color: #0ff; }
|
||||||
.sr-empty-msg { color: #888; font-style: italic; padding: 15px; }
|
.sr-empty-msg { color: #888; font-style: italic; padding: 15px; }
|
||||||
@ -345,11 +604,17 @@ return view.extend({
|
|||||||
.sr-col-url { flex: 2; min-width: 150px; font-size: 0.85em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.sr-col-url { flex: 2; min-width: 150px; font-size: 0.85em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.sr-col-url a { color: #0099cc; text-decoration: none; }
|
.sr-col-url a { color: #0099cc; text-decoration: none; }
|
||||||
.sr-col-url a:hover { text-decoration: underline; }
|
.sr-col-url a:hover { text-decoration: underline; }
|
||||||
.sr-col-badges { width: 50px; display: flex; gap: 4px; }
|
.sr-col-badges { width: 80px; display: flex; gap: 4px; }
|
||||||
.sr-col-qr { width: 36px; }
|
.sr-col-qr { width: 60px; display: flex; gap: 4px; }
|
||||||
.sr-col-action { width: 36px; }
|
.sr-col-action { width: 36px; }
|
||||||
|
|
||||||
.sr-badge { font-size: 0.85em; }
|
.sr-badge { font-size: 0.85em; cursor: help; }
|
||||||
|
.sr-badge-ok { opacity: 1; }
|
||||||
|
.sr-badge-warning { animation: pulse 2s infinite; }
|
||||||
|
.sr-badge-critical, .sr-badge-expired { animation: pulse 1s infinite; }
|
||||||
|
.sr-badge-missing, .sr-badge-none { opacity: 0.5; }
|
||||||
|
.sr-badge-failed { opacity: 1; }
|
||||||
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||||||
|
|
||||||
.sr-btn { border: none; background: transparent; cursor: pointer; font-size: 1em; padding: 4px 8px; border-radius: 4px; transition: all 0.15s; }
|
.sr-btn { border: none; background: transparent; cursor: pointer; font-size: 1em; padding: 4px 8px; border-radius: 4px; transition: all 0.15s; }
|
||||||
.sr-btn:hover { background: rgba(0,0,0,0.1); }
|
.sr-btn:hover { background: rgba(0,0,0,0.1); }
|
||||||
@ -359,6 +624,8 @@ return view.extend({
|
|||||||
.sr-btn-unpublish:hover { background: rgba(239,68,68,0.15); }
|
.sr-btn-unpublish:hover { background: rgba(239,68,68,0.15); }
|
||||||
.sr-btn-qr { color: #0099cc; }
|
.sr-btn-qr { color: #0099cc; }
|
||||||
.sr-btn-qr:hover { background: rgba(0,153,204,0.15); }
|
.sr-btn-qr:hover { background: rgba(0,153,204,0.15); }
|
||||||
|
.sr-btn-check { color: #8b5cf6; font-size: 0.9em; }
|
||||||
|
.sr-btn-check:hover { background: rgba(139,92,246,0.15); }
|
||||||
.sr-btn-regen { margin-left: 10px; font-size: 0.85em; }
|
.sr-btn-regen { margin-left: 10px; font-size: 0.85em; }
|
||||||
|
|
||||||
.sr-qr-modal { display: flex; gap: 30px; justify-content: center; flex-wrap: wrap; padding: 20px 0; }
|
.sr-qr-modal { display: flex; gap: 30px; justify-content: center; flex-wrap: wrap; padding: 20px 0; }
|
||||||
@ -380,6 +647,8 @@ return view.extend({
|
|||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sr-row { flex-wrap: wrap; }
|
.sr-row { flex-wrap: wrap; }
|
||||||
.sr-col-url { flex-basis: 100%; order: 10; margin-top: 5px; }
|
.sr-col-url { flex-basis: 100%; order: 10; margin-top: 5px; }
|
||||||
|
.sr-wizard-form { flex-direction: column; }
|
||||||
|
.sr-wizard-input { width: 100%; }
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -880,6 +880,305 @@ _add_category() {
|
|||||||
json_close_object
|
json_close_object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Helper: Check DNS resolution for a domain
|
||||||
|
check_dns_resolution() {
|
||||||
|
local domain="$1"
|
||||||
|
local expected_ip="$2"
|
||||||
|
|
||||||
|
# Try nslookup first (most common on OpenWrt)
|
||||||
|
local resolved_ip
|
||||||
|
if command -v nslookup >/dev/null 2>&1; then
|
||||||
|
resolved_ip=$(nslookup "$domain" 2>/dev/null | grep -A1 "Name:" | grep "Address" | head -1 | awk '{print $2}')
|
||||||
|
[ -z "$resolved_ip" ] && resolved_ip=$(nslookup "$domain" 2>/dev/null | grep "Address" | tail -1 | awk '{print $2}' | grep -v "^$")
|
||||||
|
elif command -v host >/dev/null 2>&1; then
|
||||||
|
resolved_ip=$(host "$domain" 2>/dev/null | grep "has address" | head -1 | awk '{print $4}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$resolved_ip" ]; then
|
||||||
|
echo "$resolved_ip"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Get WAN IP address
|
||||||
|
get_wan_ip() {
|
||||||
|
local wan_ip=""
|
||||||
|
# Try to get WAN IP from interface
|
||||||
|
wan_ip=$(uci -q get network.wan.ipaddr)
|
||||||
|
if [ -z "$wan_ip" ]; then
|
||||||
|
# Try to get from ip command
|
||||||
|
wan_ip=$(ip -4 addr show dev eth0 2>/dev/null | grep -oE 'inet [0-9.]+' | head -1 | awk '{print $2}')
|
||||||
|
fi
|
||||||
|
if [ -z "$wan_ip" ]; then
|
||||||
|
# Try to get public IP (uses local IP for comparison)
|
||||||
|
wan_ip=$(ifconfig 2>/dev/null | grep -E "inet addr:[0-9]" | grep -v "127.0.0.1" | head -1 | sed 's/.*inet addr:\([0-9.]*\).*/\1/')
|
||||||
|
fi
|
||||||
|
echo "$wan_ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Check certificate expiry
|
||||||
|
check_cert_expiry() {
|
||||||
|
local domain="$1"
|
||||||
|
local cert_file="/srv/haproxy/certs/${domain}.pem"
|
||||||
|
|
||||||
|
if [ ! -f "$cert_file" ]; then
|
||||||
|
# Try the ACME path
|
||||||
|
cert_file="/etc/acme/${domain}_ecc/${domain}.cer"
|
||||||
|
[ ! -f "$cert_file" ] && cert_file="/etc/acme/${domain}/${domain}.cer"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$cert_file" ]; then
|
||||||
|
# Get expiry date using openssl
|
||||||
|
local expiry_date
|
||||||
|
expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2)
|
||||||
|
if [ -n "$expiry_date" ]; then
|
||||||
|
# Convert to epoch
|
||||||
|
local expiry_epoch
|
||||||
|
expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null)
|
||||||
|
local now_epoch
|
||||||
|
now_epoch=$(date +%s)
|
||||||
|
local days_left
|
||||||
|
if [ -n "$expiry_epoch" ]; then
|
||||||
|
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
|
||||||
|
echo "$days_left"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Check if external port is accessible (basic check via firewall rules)
|
||||||
|
check_port_firewall_open() {
|
||||||
|
local port="$1"
|
||||||
|
local rule_name=""
|
||||||
|
|
||||||
|
case "$port" in
|
||||||
|
80) rule_name="HAProxy-HTTP" ;;
|
||||||
|
443) rule_name="HAProxy-HTTPS" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check if firewall rule exists and is enabled
|
||||||
|
local i=0
|
||||||
|
while uci -q get firewall.@rule[$i] >/dev/null 2>&1; do
|
||||||
|
local name=$(uci -q get firewall.@rule[$i].name)
|
||||||
|
local enabled=$(uci -q get firewall.@rule[$i].enabled)
|
||||||
|
local dest_port=$(uci -q get firewall.@rule[$i].dest_port)
|
||||||
|
if [ "$name" = "$rule_name" ] || [ "$dest_port" = "$port" ]; then
|
||||||
|
if [ "$enabled" != "0" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check health status for a service
|
||||||
|
method_check_service_health() {
|
||||||
|
local service_id domain
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var service_id service_id
|
||||||
|
json_get_var domain domain ""
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$service_id" ] && [ -z "$domain" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "service_id or domain is required"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If only service_id provided, get domain from config
|
||||||
|
if [ -z "$domain" ] && [ -n "$service_id" ]; then
|
||||||
|
config_load "$UCI_CONFIG"
|
||||||
|
config_get domain "$service_id" haproxy_domain ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "service_id" "$service_id"
|
||||||
|
json_add_string "domain" "$domain"
|
||||||
|
|
||||||
|
# DNS check
|
||||||
|
json_add_object "dns"
|
||||||
|
if [ -n "$domain" ]; then
|
||||||
|
local resolved_ip
|
||||||
|
resolved_ip=$(check_dns_resolution "$domain")
|
||||||
|
if [ -n "$resolved_ip" ]; then
|
||||||
|
json_add_string "status" "ok"
|
||||||
|
json_add_string "resolved_ip" "$resolved_ip"
|
||||||
|
else
|
||||||
|
json_add_string "status" "failed"
|
||||||
|
json_add_string "error" "DNS resolution failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
json_add_string "status" "none"
|
||||||
|
fi
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
# Certificate check
|
||||||
|
json_add_object "certificate"
|
||||||
|
if [ -n "$domain" ]; then
|
||||||
|
local days_left
|
||||||
|
days_left=$(check_cert_expiry "$domain")
|
||||||
|
if [ -n "$days_left" ]; then
|
||||||
|
if [ "$days_left" -lt 0 ]; then
|
||||||
|
json_add_string "status" "expired"
|
||||||
|
elif [ "$days_left" -lt 7 ]; then
|
||||||
|
json_add_string "status" "critical"
|
||||||
|
elif [ "$days_left" -lt 30 ]; then
|
||||||
|
json_add_string "status" "warning"
|
||||||
|
else
|
||||||
|
json_add_string "status" "ok"
|
||||||
|
fi
|
||||||
|
json_add_int "days_left" "$days_left"
|
||||||
|
else
|
||||||
|
json_add_string "status" "missing"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
json_add_string "status" "none"
|
||||||
|
fi
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
# Port/Firewall check
|
||||||
|
json_add_object "firewall"
|
||||||
|
local http_open=0
|
||||||
|
local https_open=0
|
||||||
|
check_port_firewall_open 80 && http_open=1
|
||||||
|
check_port_firewall_open 443 && https_open=1
|
||||||
|
|
||||||
|
if [ "$http_open" = "1" ] && [ "$https_open" = "1" ]; then
|
||||||
|
json_add_string "status" "ok"
|
||||||
|
elif [ "$http_open" = "1" ] || [ "$https_open" = "1" ]; then
|
||||||
|
json_add_string "status" "partial"
|
||||||
|
else
|
||||||
|
json_add_string "status" "closed"
|
||||||
|
fi
|
||||||
|
json_add_boolean "http_open" "$http_open"
|
||||||
|
json_add_boolean "https_open" "$https_open"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
# HAProxy status
|
||||||
|
json_add_object "haproxy"
|
||||||
|
if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
json_add_string "status" "running"
|
||||||
|
else
|
||||||
|
json_add_string "status" "stopped"
|
||||||
|
fi
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Batch health check for all published services (for dashboard)
|
||||||
|
method_check_all_health() {
|
||||||
|
json_init
|
||||||
|
json_add_object "health"
|
||||||
|
|
||||||
|
local wan_ip
|
||||||
|
wan_ip=$(get_wan_ip)
|
||||||
|
json_add_string "wan_ip" "$wan_ip"
|
||||||
|
|
||||||
|
# Check HAProxy status
|
||||||
|
json_add_object "haproxy"
|
||||||
|
if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
json_add_string "status" "running"
|
||||||
|
else
|
||||||
|
json_add_string "status" "stopped"
|
||||||
|
fi
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
# Check Tor status
|
||||||
|
json_add_object "tor"
|
||||||
|
if pgrep -f "/usr/sbin/tor" >/dev/null 2>&1; then
|
||||||
|
json_add_string "status" "running"
|
||||||
|
else
|
||||||
|
json_add_string "status" "stopped"
|
||||||
|
fi
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
# Check firewall ports
|
||||||
|
json_add_object "firewall"
|
||||||
|
local http_open=0
|
||||||
|
local https_open=0
|
||||||
|
check_port_firewall_open 80 && http_open=1
|
||||||
|
check_port_firewall_open 443 && https_open=1
|
||||||
|
json_add_boolean "http_open" "$http_open"
|
||||||
|
json_add_boolean "https_open" "$https_open"
|
||||||
|
if [ "$http_open" = "1" ] && [ "$https_open" = "1" ]; then
|
||||||
|
json_add_string "status" "ok"
|
||||||
|
elif [ "$http_open" = "1" ] || [ "$https_open" = "1" ]; then
|
||||||
|
json_add_string "status" "partial"
|
||||||
|
else
|
||||||
|
json_add_string "status" "closed"
|
||||||
|
fi
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
# Check individual services with domains
|
||||||
|
json_add_array "services"
|
||||||
|
|
||||||
|
# Get all published services with domains from HAProxy
|
||||||
|
local vhosts_json
|
||||||
|
vhosts_json=$(ubus call luci.haproxy list_vhosts 2>/dev/null)
|
||||||
|
if [ -n "$vhosts_json" ]; then
|
||||||
|
local count
|
||||||
|
count=$(echo "$vhosts_json" | jsonfilter -e '@.vhosts[*].domain' 2>/dev/null | wc -l)
|
||||||
|
local i=0
|
||||||
|
while [ $i -lt "$count" ]; do
|
||||||
|
local domain enabled
|
||||||
|
domain=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].domain" 2>/dev/null)
|
||||||
|
enabled=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].enabled" 2>/dev/null)
|
||||||
|
|
||||||
|
i=$((i + 1))
|
||||||
|
|
||||||
|
[ -z "$domain" ] && continue
|
||||||
|
[ "$enabled" = "false" ] || [ "$enabled" = "0" ] && continue
|
||||||
|
|
||||||
|
json_add_object
|
||||||
|
json_add_string "domain" "$domain"
|
||||||
|
|
||||||
|
# DNS check
|
||||||
|
local resolved_ip
|
||||||
|
resolved_ip=$(check_dns_resolution "$domain")
|
||||||
|
if [ -n "$resolved_ip" ]; then
|
||||||
|
json_add_string "dns_status" "ok"
|
||||||
|
json_add_string "dns_ip" "$resolved_ip"
|
||||||
|
else
|
||||||
|
json_add_string "dns_status" "failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cert check
|
||||||
|
local days_left
|
||||||
|
days_left=$(check_cert_expiry "$domain")
|
||||||
|
if [ -n "$days_left" ]; then
|
||||||
|
if [ "$days_left" -lt 0 ]; then
|
||||||
|
json_add_string "cert_status" "expired"
|
||||||
|
elif [ "$days_left" -lt 7 ]; then
|
||||||
|
json_add_string "cert_status" "critical"
|
||||||
|
elif [ "$days_left" -lt 30 ]; then
|
||||||
|
json_add_string "cert_status" "warning"
|
||||||
|
else
|
||||||
|
json_add_string "cert_status" "ok"
|
||||||
|
fi
|
||||||
|
json_add_int "cert_days" "$days_left"
|
||||||
|
else
|
||||||
|
json_add_string "cert_status" "missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
# Get certificate status for service
|
# Get certificate status for service
|
||||||
method_get_certificate_status() {
|
method_get_certificate_status() {
|
||||||
local service_id
|
local service_id
|
||||||
@ -908,13 +1207,27 @@ method_get_certificate_status() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get certificate info from HAProxy
|
# Get certificate expiry info
|
||||||
local cert_info
|
local days_left
|
||||||
cert_info=$(ubus call luci.haproxy list_certificates 2>/dev/null)
|
days_left=$(check_cert_expiry "$haproxy_domain")
|
||||||
|
|
||||||
json_add_boolean "success" 1
|
json_add_boolean "success" 1
|
||||||
json_add_string "domain" "$haproxy_domain"
|
json_add_string "domain" "$haproxy_domain"
|
||||||
json_add_string "status" "unknown"
|
|
||||||
|
if [ -n "$days_left" ]; then
|
||||||
|
if [ "$days_left" -lt 0 ]; then
|
||||||
|
json_add_string "status" "expired"
|
||||||
|
elif [ "$days_left" -lt 7 ]; then
|
||||||
|
json_add_string "status" "critical"
|
||||||
|
elif [ "$days_left" -lt 30 ]; then
|
||||||
|
json_add_string "status" "warning"
|
||||||
|
else
|
||||||
|
json_add_string "status" "valid"
|
||||||
|
fi
|
||||||
|
json_add_int "days_left" "$days_left"
|
||||||
|
else
|
||||||
|
json_add_string "status" "missing"
|
||||||
|
fi
|
||||||
|
|
||||||
json_dump
|
json_dump
|
||||||
}
|
}
|
||||||
@ -978,6 +1291,8 @@ case "$1" in
|
|||||||
"get_qr_data": { "service_id": "string", "url_type": "string" },
|
"get_qr_data": { "service_id": "string", "url_type": "string" },
|
||||||
"list_categories": {},
|
"list_categories": {},
|
||||||
"get_certificate_status": { "service_id": "string" },
|
"get_certificate_status": { "service_id": "string" },
|
||||||
|
"check_service_health": { "service_id": "string", "domain": "string" },
|
||||||
|
"check_all_health": {},
|
||||||
"get_landing_config": {},
|
"get_landing_config": {},
|
||||||
"save_landing_config": { "auto_regen": "boolean" }
|
"save_landing_config": { "auto_regen": "boolean" }
|
||||||
}
|
}
|
||||||
@ -996,6 +1311,8 @@ EOF
|
|||||||
get_qr_data) method_get_qr_data ;;
|
get_qr_data) method_get_qr_data ;;
|
||||||
list_categories) method_list_categories ;;
|
list_categories) method_list_categories ;;
|
||||||
get_certificate_status) method_get_certificate_status ;;
|
get_certificate_status) method_get_certificate_status ;;
|
||||||
|
check_service_health) method_check_service_health ;;
|
||||||
|
check_all_health) method_check_all_health ;;
|
||||||
get_landing_config) method_get_landing_config ;;
|
get_landing_config) method_get_landing_config ;;
|
||||||
save_landing_config) method_save_landing_config ;;
|
save_landing_config) method_save_landing_config ;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
"list_categories",
|
"list_categories",
|
||||||
"get_qr_data",
|
"get_qr_data",
|
||||||
"get_certificate_status",
|
"get_certificate_status",
|
||||||
|
"check_service_health",
|
||||||
|
"check_all_health",
|
||||||
"get_landing_config"
|
"get_landing_config"
|
||||||
],
|
],
|
||||||
"luci.haproxy": [
|
"luci.haproxy": [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user