secubox-openwrt/package/secubox/luci-app-watchdog/htdocs/luci-static/resources/view/watchdog/status.js
CyberMind-FR 66d9fbc6c0 feat(watchdog): Add service health monitor with auto-recovery
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>
2026-03-12 06:46:53 +01:00

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
});