New packages for monitoring and auto-restarting critical services: secubox-app-watchdog: - watchdogctl CLI: status, check, check-recover, watch, restart-* - Monitors LXC containers: haproxy, mitmproxy-in/out, streamlit - Monitors host services: crowdsec, uhttpd, dnsmasq - Checks HTTPS endpoints: gk2.secubox.in, admin.gk2, lldh360.maegia.tv - Auto-recovery with alert cooldown and log rotation - Procd service + cron fallback for redundancy luci-app-watchdog: - Real-time dashboard with 10s polling - Container/service tables with restart buttons - Endpoint health indicators - Alert log viewer with refresh/clear - RPCD backend: status, restart_*, check, get_logs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
381 lines
15 KiB
JavaScript
381 lines
15 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require rpc';
|
|
'require ui';
|
|
'require poll';
|
|
'require dom';
|
|
|
|
var callGetStatus = rpc.declare({
|
|
object: 'luci.watchdog',
|
|
method: 'status',
|
|
expect: {}
|
|
});
|
|
|
|
var callGetLogs = rpc.declare({
|
|
object: 'luci.watchdog',
|
|
method: 'get_logs',
|
|
params: ['lines'],
|
|
expect: {}
|
|
});
|
|
|
|
var callRestartContainer = rpc.declare({
|
|
object: 'luci.watchdog',
|
|
method: 'restart_container',
|
|
params: ['name'],
|
|
expect: {}
|
|
});
|
|
|
|
var callRestartService = rpc.declare({
|
|
object: 'luci.watchdog',
|
|
method: 'restart_service',
|
|
params: ['name'],
|
|
expect: {}
|
|
});
|
|
|
|
var callCheck = rpc.declare({
|
|
object: 'luci.watchdog',
|
|
method: 'check',
|
|
expect: {}
|
|
});
|
|
|
|
var callClearLogs = rpc.declare({
|
|
object: 'luci.watchdog',
|
|
method: 'clear_logs',
|
|
expect: {}
|
|
});
|
|
|
|
function renderStatusBadge(state, critical) {
|
|
var color = state === 'running' ? '#00ff88' : (critical ? '#ff0066' : '#ffaa00');
|
|
var text = state === 'running' ? 'RUNNING' : 'STOPPED';
|
|
return E('span', {
|
|
'style': 'background: ' + color + '; color: #000; padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 11px;'
|
|
}, text);
|
|
}
|
|
|
|
function renderHealthBadge(healthy) {
|
|
var color = healthy ? '#00ff88' : '#ff0066';
|
|
var text = healthy ? 'HEALTHY' : 'UNHEALTHY';
|
|
return E('span', {
|
|
'style': 'background: ' + color + '; color: #000; padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 11px;'
|
|
}, text);
|
|
}
|
|
|
|
function renderCriticalBadge(critical) {
|
|
if (!critical) return '';
|
|
return E('span', {
|
|
'style': 'background: #ff0066; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 10px; margin-left: 8px;'
|
|
}, 'CRITICAL');
|
|
}
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
callGetStatus(),
|
|
callGetLogs(30)
|
|
]);
|
|
},
|
|
|
|
pollStatus: function() {
|
|
var self = this;
|
|
poll.add(function() {
|
|
return callGetStatus().then(function(status) {
|
|
self.updateDashboard(status);
|
|
});
|
|
}, 10);
|
|
},
|
|
|
|
updateDashboard: function(status) {
|
|
// Update watchdog status
|
|
var watchdogStatus = document.getElementById('watchdog-status');
|
|
if (watchdogStatus) {
|
|
var running = status.running;
|
|
watchdogStatus.innerHTML = '';
|
|
watchdogStatus.appendChild(E('span', {
|
|
'style': 'color: ' + (running ? '#00ff88' : '#ff0066') + '; font-weight: bold;'
|
|
}, running ? 'ACTIVE' : 'INACTIVE'));
|
|
}
|
|
|
|
// Update containers
|
|
var containersTable = document.getElementById('containers-body');
|
|
if (containersTable && status.containers) {
|
|
containersTable.innerHTML = '';
|
|
status.containers.forEach(function(c) {
|
|
var row = E('tr', {}, [
|
|
E('td', {}, c.name),
|
|
E('td', {}, [renderStatusBadge(c.state, c.critical), renderCriticalBadge(c.critical)]),
|
|
E('td', {}, c.pid > 0 ? String(c.pid) : '-'),
|
|
E('td', {}, [
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-action',
|
|
'click': ui.createHandlerFn(this, 'handleRestartContainer', c.name),
|
|
'style': 'padding: 2px 8px; font-size: 11px;'
|
|
}, 'Restart')
|
|
])
|
|
]);
|
|
containersTable.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Update services
|
|
var servicesTable = document.getElementById('services-body');
|
|
if (servicesTable && status.services) {
|
|
servicesTable.innerHTML = '';
|
|
status.services.forEach(function(s) {
|
|
var row = E('tr', {}, [
|
|
E('td', {}, s.name),
|
|
E('td', {}, s.process),
|
|
E('td', {}, [renderStatusBadge(s.state, s.critical), renderCriticalBadge(s.critical)]),
|
|
E('td', {}, s.pid > 0 ? String(s.pid) : '-'),
|
|
E('td', {}, [
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-action',
|
|
'click': ui.createHandlerFn(this, 'handleRestartService', s.name),
|
|
'style': 'padding: 2px 8px; font-size: 11px;'
|
|
}, 'Restart')
|
|
])
|
|
]);
|
|
servicesTable.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Update endpoints
|
|
var endpointsTable = document.getElementById('endpoints-body');
|
|
if (endpointsTable && status.endpoints) {
|
|
endpointsTable.innerHTML = '';
|
|
status.endpoints.forEach(function(e) {
|
|
var row = E('tr', {}, [
|
|
E('td', {}, e.name),
|
|
E('td', {}, e.host),
|
|
E('td', {}, 'HTTP ' + e.code),
|
|
E('td', {}, renderHealthBadge(e.healthy))
|
|
]);
|
|
endpointsTable.appendChild(row);
|
|
});
|
|
}
|
|
},
|
|
|
|
handleRestartContainer: function(name) {
|
|
var self = this;
|
|
ui.showModal('Restarting Container', [
|
|
E('p', { 'class': 'spinning' }, 'Restarting ' + name + '...')
|
|
]);
|
|
|
|
return callRestartContainer(name).then(function(result) {
|
|
ui.hideModal();
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', {}, 'Container ' + name + ' restarted successfully'), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, 'Failed to restart ' + name + ': ' + (result.error || 'Unknown error')), 'error');
|
|
}
|
|
return callGetStatus().then(function(status) {
|
|
self.updateDashboard(status);
|
|
});
|
|
});
|
|
},
|
|
|
|
handleRestartService: function(name) {
|
|
var self = this;
|
|
ui.showModal('Restarting Service', [
|
|
E('p', { 'class': 'spinning' }, 'Restarting ' + name + '...')
|
|
]);
|
|
|
|
return callRestartService(name).then(function(result) {
|
|
ui.hideModal();
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', {}, 'Service ' + name + ' restarted successfully'), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, 'Failed to restart ' + name + ': ' + (result.error || 'Unknown error')), 'error');
|
|
}
|
|
return callGetStatus().then(function(status) {
|
|
self.updateDashboard(status);
|
|
});
|
|
});
|
|
},
|
|
|
|
handleRunCheck: function() {
|
|
var self = this;
|
|
ui.showModal('Running Health Check', [
|
|
E('p', { 'class': 'spinning' }, 'Running health check with auto-recovery...')
|
|
]);
|
|
|
|
return callCheck().then(function(result) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, 'Health check completed'), 'success');
|
|
return callGetStatus().then(function(status) {
|
|
self.updateDashboard(status);
|
|
});
|
|
});
|
|
},
|
|
|
|
handleClearLogs: function() {
|
|
return callClearLogs().then(function() {
|
|
ui.addNotification(null, E('p', {}, 'Logs cleared'), 'success');
|
|
var logsArea = document.getElementById('logs-area');
|
|
if (logsArea) {
|
|
logsArea.value = '';
|
|
}
|
|
});
|
|
},
|
|
|
|
handleRefreshLogs: function() {
|
|
return callGetLogs(50).then(function(result) {
|
|
var logsArea = document.getElementById('logs-area');
|
|
if (logsArea && result.lines) {
|
|
logsArea.value = result.lines.join('\n');
|
|
logsArea.scrollTop = logsArea.scrollHeight;
|
|
}
|
|
});
|
|
},
|
|
|
|
render: function(data) {
|
|
var status = data[0] || {};
|
|
var logs = data[1] || {};
|
|
var self = this;
|
|
|
|
var view = E('div', { 'class': 'cbi-map' }, [
|
|
E('h2', {}, 'SecuBox Watchdog'),
|
|
E('div', { 'class': 'cbi-map-descr' }, 'Service health monitoring and auto-recovery dashboard'),
|
|
|
|
// Status overview
|
|
E('div', { 'class': 'cbi-section', 'style': 'background: linear-gradient(135deg, #1a1a2e 0%, #0f0f1a 100%); border: 1px solid #333; border-radius: 8px; padding: 16px; margin-bottom: 20px;' }, [
|
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
|
|
E('div', {}, [
|
|
E('span', { 'style': 'color: #888;' }, 'Watchdog Status: '),
|
|
E('span', { 'id': 'watchdog-status', 'style': 'color: ' + (status.running ? '#00ff88' : '#ff0066') + '; font-weight: bold;' },
|
|
status.running ? 'ACTIVE' : 'INACTIVE'),
|
|
E('span', { 'style': 'color: #888; margin-left: 20px;' }, 'Check Interval: '),
|
|
E('span', { 'style': 'color: #00ffff;' }, (status.interval || 60) + 's')
|
|
]),
|
|
E('div', {}, [
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-action',
|
|
'click': ui.createHandlerFn(this, 'handleRunCheck')
|
|
}, 'Run Check Now')
|
|
])
|
|
])
|
|
]),
|
|
|
|
// Containers section
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, 'LXC Containers'),
|
|
E('table', { 'class': 'table cbi-section-table' }, [
|
|
E('thead', {}, [
|
|
E('tr', { 'class': 'tr table-titles' }, [
|
|
E('th', { 'class': 'th' }, 'Container'),
|
|
E('th', { 'class': 'th' }, 'Status'),
|
|
E('th', { 'class': 'th' }, 'PID'),
|
|
E('th', { 'class': 'th' }, 'Actions')
|
|
])
|
|
]),
|
|
E('tbody', { 'id': 'containers-body' },
|
|
(status.containers || []).map(function(c) {
|
|
return E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, c.name),
|
|
E('td', { 'class': 'td' }, [renderStatusBadge(c.state, c.critical), renderCriticalBadge(c.critical)]),
|
|
E('td', { 'class': 'td' }, c.pid > 0 ? String(c.pid) : '-'),
|
|
E('td', { 'class': 'td' }, [
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-action',
|
|
'click': ui.createHandlerFn(self, 'handleRestartContainer', c.name),
|
|
'style': 'padding: 2px 8px; font-size: 11px;'
|
|
}, 'Restart')
|
|
])
|
|
]);
|
|
})
|
|
)
|
|
])
|
|
]),
|
|
|
|
// Services section
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, 'Host Services'),
|
|
E('table', { 'class': 'table cbi-section-table' }, [
|
|
E('thead', {}, [
|
|
E('tr', { 'class': 'tr table-titles' }, [
|
|
E('th', { 'class': 'th' }, 'Service'),
|
|
E('th', { 'class': 'th' }, 'Process'),
|
|
E('th', { 'class': 'th' }, 'Status'),
|
|
E('th', { 'class': 'th' }, 'PID'),
|
|
E('th', { 'class': 'th' }, 'Actions')
|
|
])
|
|
]),
|
|
E('tbody', { 'id': 'services-body' },
|
|
(status.services || []).map(function(s) {
|
|
return E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, s.name),
|
|
E('td', { 'class': 'td' }, s.process),
|
|
E('td', { 'class': 'td' }, [renderStatusBadge(s.state, s.critical), renderCriticalBadge(s.critical)]),
|
|
E('td', { 'class': 'td' }, s.pid > 0 ? String(s.pid) : '-'),
|
|
E('td', { 'class': 'td' }, [
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-action',
|
|
'click': ui.createHandlerFn(self, 'handleRestartService', s.name),
|
|
'style': 'padding: 2px 8px; font-size: 11px;'
|
|
}, 'Restart')
|
|
])
|
|
]);
|
|
})
|
|
)
|
|
])
|
|
]),
|
|
|
|
// Endpoints section
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, 'HTTPS Endpoints'),
|
|
E('table', { 'class': 'table cbi-section-table' }, [
|
|
E('thead', {}, [
|
|
E('tr', { 'class': 'tr table-titles' }, [
|
|
E('th', { 'class': 'th' }, 'Name'),
|
|
E('th', { 'class': 'th' }, 'Host'),
|
|
E('th', { 'class': 'th' }, 'Response'),
|
|
E('th', { 'class': 'th' }, 'Health')
|
|
])
|
|
]),
|
|
E('tbody', { 'id': 'endpoints-body' },
|
|
(status.endpoints || []).map(function(e) {
|
|
return E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, e.name),
|
|
E('td', { 'class': 'td' }, e.host),
|
|
E('td', { 'class': 'td' }, 'HTTP ' + e.code),
|
|
E('td', { 'class': 'td' }, renderHealthBadge(e.healthy))
|
|
]);
|
|
})
|
|
)
|
|
])
|
|
]),
|
|
|
|
// Logs section
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
|
|
E('h3', {}, 'Alert Logs'),
|
|
E('div', {}, [
|
|
E('button', {
|
|
'class': 'cbi-button',
|
|
'click': ui.createHandlerFn(this, 'handleRefreshLogs'),
|
|
'style': 'margin-right: 8px;'
|
|
}, 'Refresh'),
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-negative',
|
|
'click': ui.createHandlerFn(this, 'handleClearLogs')
|
|
}, 'Clear')
|
|
])
|
|
]),
|
|
E('textarea', {
|
|
'id': 'logs-area',
|
|
'readonly': 'readonly',
|
|
'style': 'width: 100%; height: 200px; background: #0f0f1a; color: #00ff88; font-family: monospace; font-size: 12px; border: 1px solid #333; border-radius: 4px; padding: 8px;'
|
|
}, (logs.lines || []).join('\n'))
|
|
])
|
|
]);
|
|
|
|
// Start polling
|
|
this.pollStatus();
|
|
|
|
return view;
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|