This commit is contained in:
Your Name
2025-05-26 17:42:29 +02:00
parent 4fc06ccce0
commit cd8cf349dd
9 changed files with 1207 additions and 295 deletions

View File

@@ -71,39 +71,41 @@
<!-- config_scripts_table -->
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_NPM" onchange="update_config_scripts_sqlite('NPM/get_data_modbus_v3.py', this.checked)">
<label class="form-check-label" for="check_NPM">
Next PM
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_NPM_5channels" onchange="update_config_sqlite('npm_5channel', this.checked)">
<label class="form-check-label" for="check_NPM_5channels">
Next PM send 5 channels (no script)
Send Next PM 5 channels data
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_bme280" onchange="update_config_scripts_sqlite('BME280/get_data_v2.py', this.checked)">
<input class="form-check-input" type="checkbox" value="" id="check_bme280" onchange="update_config_sqlite('BME280', this.checked)">
<label class="form-check-label" for="check_bme280">
Sonde temp/hum (BME280)
Send temp/hum data (BME280)
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_CO2" onchange="update_config_scripts_sqlite('MH-Z19/write_data.py', this.checked)">
<input class="form-check-input" type="checkbox" value="" id="check_CO2" onchange="update_config_sqlite('MHZ19', this.checked)">
<label class="form-check-label" for="check_CO2">
Sonde CO2 (MH-Z19)
Send CO2 data (MH-Z19)
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_sensirionSFA30" onchange="update_config_scripts_sqlite('sensirion/SFA30_read.py', this.checked)">
<input class="form-check-input" type="checkbox" value="" id="check_sensirionSFA30" onchange="update_config_sqlite('SFA30', this.checked)">
<label class="form-check-label" for="check_sensirionSFA30">
Sonde Formaldehyde (Sensiririon SFA30)
Send Formaldehyde data (Sensiririon SFA30)
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="check_uSpot" onchange="update_config_sqlite('send_uSpot', this.checked)" disabled>
<label class="form-check-label" for="check_uSpot">
Send to uSpot
</label>
</div>
@@ -158,6 +160,44 @@
</div>
<!-- SYSTEMD SERVICES SECTION -->
<div class="row mb-3">
<div class="col-lg-8 col-12">
<h4 class="mt-4">SystemD Services</h4>
<div id="services-table" class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="fw-bold">Service Status</span>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="refreshServices()">
<svg width="14" height="14" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>
Refresh
</button>
</div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 20%">Service</th>
<th style="width: 25%">Description</th>
<th style="width: 15%">Frequency</th>
<th style="width: 10%">Status</th>
<th style="width: 10%">Enabled</th>
<th style="width: 20%">Actions</th>
</tr>
</thead>
<tbody id="services-tbody">
<tr>
<td colspan="6" class="text-center py-3">Loading services...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- toast -->
@@ -218,126 +258,111 @@
window.onload = function() {
//NEW way to get config (SQLite)
$.ajax({
url: 'launcher.php?type=get_config_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config table:");
console.log(response);
//device name
const deviceName = document.getElementById("device_name");
deviceName.value = response.deviceName;
//device name_side bar
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = response.deviceName;
});
//device ID
const deviceID = response.deviceID.trim().toUpperCase();
const device_ID = document.getElementById("device_ID");
device_ID.value = response.deviceID.toUpperCase();
//nextPM send 5 channels
const checkbox_nmp5channels = document.getElementById("check_NPM_5channels");
checkbox_nmp5channels.checked = response.npm_5channel;
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX
//getting config_scripts table
//NEW way to get config (SQLite)
$.ajax({
url: 'launcher.php?type=get_config_scripts_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config scripts table:");
console.log(response);
url: 'launcher.php?type=get_config_sqlite',
dataType:'json',
//dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Getting SQLite config table:");
console.log(response);
//device name
const deviceName = document.getElementById("device_name");
deviceName.value = response.deviceName;
//device name_side bar
const elements = document.querySelectorAll('.sideBar_sensorName');
elements.forEach((element) => {
element.innerText = response.deviceName;
});
//device ID
const deviceID = response.deviceID.trim().toUpperCase();
const device_ID = document.getElementById("device_ID");
device_ID.value = response.deviceID.toUpperCase();
const checkbox_NPM = document.getElementById("check_NPM");
const checkbox_bme = document.getElementById("check_bme280");
const checkbox_CO2 = document.getElementById("check_CO2");
const checkbox_SFA30 = document.getElementById("check_sensirionSFA30");
checkbox_NPM.checked = response["NPM/get_data_modbus_v3.py"];
checkbox_bme.checked = response["BME280/get_data_v2.py"];
checkbox_SFA30.checked = response["sensirion/SFA30_read.py"];
checkbox_CO2.checked = response["MH-Z19/write_data.py"];
//si sonde envea is true
if (response["envea/read_value_v2.py"]) {
add_sondeEnveaContainer();
const checkbox_nmp5channels = document.getElementById("check_NPM_5channels");
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX
const checkbox_bme = document.getElementById("check_bme280");
const checkbox_CO2 = document.getElementById("check_CO2");
const checkbox_SFA30 = document.getElementById("check_sensirionSFA30");
//get system time and RTC module
$.ajax({
url: 'launcher.php?type=sys_RTC_module_time',
dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
// Update the input fields with the received JSON data
document.getElementById("sys_local_time").value = response.system_local_time;
document.getElementById("sys_UTC_time").value = response.system_utc_time;
document.getElementById("RTC_utc_time").value = response.rtc_module_time;
checkbox_nmp5channels.checked = response.npm_5channel;
checkbox_bme.checked = response["BME280"];
checkbox_SFA30.checked = response["SFA30"];
checkbox_CO2.checked = response["MH-Z19"];
// Get the time difference
const timeDiff = response.time_difference_seconds;
//si sonde envea is true
if (response["envea"]) {
add_sondeEnveaContainer();
}
// Reference to the alert container
const alertContainer = document.getElementById("alert_container");
// Remove any previous alert
alertContainer.innerHTML = "";
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end AJAX
// Add an alert based on time difference
if (typeof timeDiff === "number") {
if (timeDiff >= 0 && timeDiff <= 10) {
alertContainer.innerHTML = `
<div class="alert alert-success" role="alert">
RTC and system time are in sync (Difference: ${timeDiff} sec).
</div>`;
} else if (timeDiff > 10) {
alertContainer.innerHTML = `
<div class="alert alert-danger" role="alert">
RTC time is out of sync! (Difference: ${timeDiff} sec).
</div>`;
}
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end ajax
//get system time and RTC module
$.ajax({
url: 'launcher.php?type=sys_RTC_module_time',
dataType: 'json', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log(response);
// Update the input fields with the received JSON data
document.getElementById("sys_local_time").value = response.system_local_time;
document.getElementById("sys_UTC_time").value = response.system_utc_time;
document.getElementById("RTC_utc_time").value = response.rtc_module_time;
//get local RTC
$.ajax({
url: 'launcher.php?type=RTC_time',
dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Local RTC: " + response);
const RTC_Element = document.getElementById("RTC_time");
RTC_Element.textContent = response;
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end ajax
// Get the time difference
const timeDiff = response.time_difference_seconds;
// Reference to the alert container
const alertContainer = document.getElementById("alert_container");
// Remove any previous alert
alertContainer.innerHTML = "";
// Add an alert based on time difference
if (typeof timeDiff === "number") {
if (timeDiff >= 0 && timeDiff <= 10) {
alertContainer.innerHTML = `
<div class="alert alert-success" role="alert">
RTC and system time are in sync (Difference: ${timeDiff} sec).
</div>`;
} else if (timeDiff > 10) {
alertContainer.innerHTML = `
<div class="alert alert-danger" role="alert">
RTC time is out of sync! (Difference: ${timeDiff} sec).
</div>`;
}
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end ajax
//get local RTC
$.ajax({
url: 'launcher.php?type=RTC_time',
dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
success: function(response) {
console.log("Local RTC: " + response);
const RTC_Element = document.getElementById("RTC_time");
RTC_Element.textContent = response;
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});//end ajax
// Load services on page load
refreshServices();
} //end windows on load
@@ -391,78 +416,6 @@ function update_config_sqlite(param, value){
});
}
function update_config_scripts_sqlite(param, value) {
console.log("Updating scripts sqlite ", param, " : ", value);
const toastLiveExample = document.getElementById('liveToast')
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: 'launcher.php?type=update_config_scripts_sqlite&param=' + param + '&value=' + value,
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log(response);
// Format the response nicely
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Parameter: ${response.script_path || param}<br>
Value: ${response.enabled !== undefined ? response.enabled : value}<br>
${response.message || ''}
`;
if (response.script_path == "envea/read_value_v2.py") {
console.log("envea sondes activated");
add_sondeEnveaContainer();
}
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
${response.error || 'Unknown error'}<br>
Parameter: ${response.script_path || param}
`;
}
// Update the toast body with formatted content
toastBody.innerHTML = formattedMessage;
// Show the toast
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
toastBootstrap.show()
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function update_config(param, value){
console.log("Updating ",param," : ", value);
$.ajax({
url: 'launcher.php?type=update_config&param='+param+'&value='+value,
dataType: 'text', // Specify that you expect a JSON response
method: 'GET', // Use GET or POST depending on your needs
cache: false, // Prevent AJAX from caching
success: function(response) {
console.log(response);
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function updateGitPull(){
console.log("Updating device (git pull)");
@@ -537,6 +490,253 @@ function set_RTC_withBrowser(){
}
/*
____ _ __ __ _
/ ___| ___ _ ____ _(_) ___ ___| \/ | __ _ _ __ __ _ __ _ ___ _ __ ___ ___ _ __ | |_
\___ \ / _ \ '__\ \ / / |/ __/ _ \ |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '_ ` _ \ / _ \ '_ \| __|
___) | __/ | \ V /| | (_| __/ | | | (_| | | | | (_| | (_| | __/ | | | | | __/ | | | |_
|____/ \___|_| \_/ |_|\___\___|_| |_|\__,_|_| |_|\__,_|\__, |\___|_| |_| |_|\___|_| |_|\__|
|___/
*/
function refreshServices() {
console.log("Refreshing services status");
$.ajax({
url: 'launcher.php?type=get_systemd_services',
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log("Services data:", response);
if (response.success) {
displayServices(response.services);
} else {
showServiceError("Failed to load services: " + response.error);
}
},
error: function(xhr, status, error) {
console.error('Failed to load services:', error);
showServiceError("Failed to load services: " + error);
}
});
}
function displayServices(services) {
const tbody = document.getElementById('services-tbody');
tbody.innerHTML = '';
services.forEach(function(service) {
const row = document.createElement('tr');
// Service name
const nameCell = document.createElement('td');
nameCell.textContent = service.display_name || service.name;
row.appendChild(nameCell);
// Description
const descCell = document.createElement('td');
descCell.textContent = service.description || 'No description available';
descCell.className = 'text-muted small';
row.appendChild(descCell);
// Frequency
const freqCell = document.createElement('td');
freqCell.textContent = service.frequency || 'Unknown';
freqCell.className = 'text-primary small fw-bold';
row.appendChild(freqCell);
// Status
const statusCell = document.createElement('td');
const statusBadge = document.createElement('span');
statusBadge.className = `badge ${service.active ? 'bg-success' : 'bg-danger'}`;
statusBadge.textContent = service.active ? 'Running' : 'Stopped';
statusCell.appendChild(statusBadge);
row.appendChild(statusCell);
// Enabled
const enabledCell = document.createElement('td');
const enabledBadge = document.createElement('span');
enabledBadge.className = `badge ${service.enabled ? 'bg-info' : 'bg-secondary'}`;
enabledBadge.textContent = service.enabled ? 'Enabled' : 'Disabled';
enabledCell.appendChild(enabledBadge);
row.appendChild(enabledCell);
// Actions
const actionsCell = document.createElement('td');
// Restart button
const restartBtn = document.createElement('button');
restartBtn.className = 'btn btn-sm btn-warning me-2';
restartBtn.innerHTML = '<i class="bi bi-arrow-clockwise"></i> Restart';
restartBtn.onclick = function() {
restartService(service.name);
};
actionsCell.appendChild(restartBtn);
// Enable/Disable button
const toggleBtn = document.createElement('button');
toggleBtn.className = `btn btn-sm ${service.enabled ? 'btn-danger' : 'btn-success'}`;
toggleBtn.innerHTML = service.enabled ? '<i class="bi bi-stop"></i> Disable' : '<i class="bi bi-play"></i> Enable';
toggleBtn.onclick = function() {
toggleService(service.name, !service.enabled);
};
actionsCell.appendChild(toggleBtn);
row.appendChild(actionsCell);
tbody.appendChild(row);
});
}
function showServiceError(message) {
const tbody = document.getElementById('services-tbody');
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-danger">
<i class="bi bi-exclamation-triangle"></i> ${message}
</td>
</tr>
`;
}
function restartService(serviceName) {
console.log(`Restarting service: ${serviceName}`);
if (!confirm(`Are you sure you want to restart ${serviceName}?`)) {
return;
}
const toastLiveExample = document.getElementById('liveToast');
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: 'launcher.php?type=restart_systemd_service&service=' + encodeURIComponent(serviceName),
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log('Service restart response:', response);
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Service: ${serviceName}<br>
${response.message || 'Service restarted successfully'}
`;
// Refresh services after a short delay
setTimeout(refreshServices, 2000);
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
Service: ${serviceName}<br>
${response.error || 'Unknown error occurred'}
`;
}
// Update and show toast
toastBody.innerHTML = formattedMessage;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
},
error: function(xhr, status, error) {
console.error('Failed to restart service:', error);
// Show error toast
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
toastBody.innerHTML = `
<strong>Request Failed!</strong><br>
Service: ${serviceName}<br>
Error: ${error}
`;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
}
});
}
function toggleService(serviceName, enable) {
const action = enable ? 'enable' : 'disable';
console.log(`${action} service: ${serviceName}`);
if (!confirm(`Are you sure you want to ${action} ${serviceName}?`)) {
return;
}
const toastLiveExample = document.getElementById('liveToast');
const toastBody = toastLiveExample.querySelector('.toast-body');
$.ajax({
url: 'launcher.php?type=toggle_systemd_service&service=' + encodeURIComponent(serviceName) + '&enable=' + enable,
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log('Service toggle response:', response);
let formattedMessage = '';
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
Service: ${serviceName}<br>
${response.message || `Service ${action}d successfully`}
`;
// Refresh services after a short delay
setTimeout(refreshServices, 2000);
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
Service: ${serviceName}<br>
${response.error || 'Unknown error occurred'}
`;
}
// Update and show toast
toastBody.innerHTML = formattedMessage;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
},
error: function(xhr, status, error) {
console.error('Failed to toggle service:', error);
// Show error toast
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
toastBody.innerHTML = `
<strong>Request Failed!</strong><br>
Service: ${serviceName}<br>
Error: ${error}
`;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
}
});
}
</script>
</body>

View File

@@ -664,3 +664,215 @@ if ($type == "wifi_scan_old") {
echo $json_data;
}
/*
____ _ ____ _ __ __ _
/ ___| _ _ ___| |_ ___ _ __ ___ | _ \ / ___| ___ _ ____ _(_) ___ ___| \/ | __ _ _ __ __ _ __ _ ___ _ __ ___ ___ _ __ | |_
\___ \| | | / __| __/ _ \ '_ ` _ \| | | | \___ \ / _ \ '__\ \ / / |/ __/ _ \ |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '_ ` _ \ / _ \ '_ \| __|
___) | |_| \__ \ || __/ | | | | | |_| | ___) | __/ | \ V /| | (_| __/ | | | (_| | | | | (_| | (_| | __/ | | | | | __/ | | | |_
|____/ \__, |___/\__\___|_| |_| |_|____/ |____/ \___|_| \_/ |_|\___\___|_| |_|\__,_|_| |_|\__,_|\__, |\___|_| |_| |_|\___|_| |_|\__|
|___/ |___/
*/
// Get systemd services status
if ($type == "get_systemd_services") {
try {
// List of NebuleAir services to monitor with descriptions and frequencies
$services = [
'moduleair-npm-data.timer' => [
'description' => 'Collects particulate matter data from NextPM sensor',
'frequency' => 'Every 10 seconds'
],
'moduleair-co2-data.timer' => [
'description' => 'Reads environmental data from Envea sensors',
'frequency' => 'Every 10 seconds'
],
'moduleair-sara-data.timer' => [
'description' => 'Transmits collected data via 4G cellular modem',
'frequency' => 'Every 60 seconds'
],
'moduleair-bme280-data.timer' => [
'description' => 'Monitors temperature and humidity (BME280)',
'frequency' => 'Every 2 minutes'
],
'moduleair-db-cleanup-data.timer' => [
'description' => 'Cleans up old data from database',
'frequency' => 'Daily'
]
];
$serviceStatus = [];
foreach ($services as $service => $serviceInfo) {
// Get service active status
$activeCmd = "systemctl is-active " . escapeshellarg($service) . " 2>/dev/null";
$activeStatus = trim(shell_exec($activeCmd));
$isActive = ($activeStatus === 'active');
// Get service enabled status
$enabledCmd = "systemctl is-enabled " . escapeshellarg($service) . " 2>/dev/null";
$enabledStatus = trim(shell_exec($enabledCmd));
$isEnabled = ($enabledStatus === 'enabled');
// Clean up service name for display
$displayName = str_replace(['.timer', 'nebuleair-', '-data'], '', $service);
$displayName = ucfirst(str_replace('-', ' ', $displayName));
$serviceStatus[] = [
'name' => $service,
'display_name' => $displayName,
'description' => $serviceInfo['description'],
'frequency' => $serviceInfo['frequency'],
'active' => $isActive,
'enabled' => $isEnabled
];
}
echo json_encode([
'success' => true,
'services' => $serviceStatus
], JSON_PRETTY_PRINT);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
// Restart a systemd service
if ($type == "restart_systemd_service") {
$service = $_GET['service'] ?? null;
if (empty($service)) {
echo json_encode([
'success' => false,
'error' => 'No service specified'
]);
exit;
}
// Validate service name (security check)
$allowedServices = [
'moduleair-npm-data.timer',
'moduleair-sara-data.timer',
'nebuleair-bme280-data.timer',
'moduleair-co2-data.timer',
'moduleair-db-cleanup-data.timer'
];
if (!in_array($service, $allowedServices)) {
echo json_encode([
'success' => false,
'error' => 'Invalid service name'
]);
exit;
}
try {
// Restart the service
$command = "sudo systemctl restart " . escapeshellarg($service) . " 2>&1";
$output = shell_exec($command);
// Check if restart was successful
$statusCmd = "systemctl is-active " . escapeshellarg($service) . " 2>/dev/null";
$status = trim(shell_exec($statusCmd));
if ($status === 'active' || empty($output)) {
echo json_encode([
'success' => true,
'message' => "Service $service restarted successfully"
]);
} else {
echo json_encode([
'success' => false,
'error' => "Failed to restart service: $output"
]);
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
// Enable/disable a systemd service
if ($type == "toggle_systemd_service") {
$service = $_GET['service'] ?? null;
$enable = $_GET['enable'] ?? null;
if (empty($service) || $enable === null) {
echo json_encode([
'success' => false,
'error' => 'Missing service name or enable parameter'
]);
exit;
}
// Validate service name (security check)
$allowedServices = [
'moduleair-npm-data.timer',
'moduleair-sara-data.timer',
'nebuleair-bme280-data.timer',
'moduleair-co2-data.timer',
'moduleair-db-cleanup-data.timer'
];
if (!in_array($service, $allowedServices)) {
echo json_encode([
'success' => false,
'error' => 'Invalid service name'
]);
exit;
}
try {
$enable = filter_var($enable, FILTER_VALIDATE_BOOLEAN);
$action = $enable ? 'enable' : 'disable';
// Enable/disable the service
$command = "sudo systemctl $action " . escapeshellarg($service) . " 2>&1";
$output = shell_exec($command);
// If disabling, also stop the service
if (!$enable) {
$stopCommand = "sudo systemctl stop " . escapeshellarg($service) . " 2>&1";
$stopOutput = shell_exec($stopCommand);
}
// If enabling, also start the service
if ($enable) {
$startCommand = "sudo systemctl start " . escapeshellarg($service) . " 2>&1";
$startOutput = shell_exec($startCommand);
}
// Check if the operation was successful
$statusCmd = "systemctl is-enabled " . escapeshellarg($service) . " 2>/dev/null";
$status = trim(shell_exec($statusCmd));
$expectedStatus = $enable ? 'enabled' : 'disabled';
if ($status === $expectedStatus) {
echo json_encode([
'success' => true,
'message' => "Service $service " . ($enable ? 'enabled and started' : 'disabled and stopped') . " successfully"
]);
} else {
echo json_encode([
'success' => false,
'error' => "Failed to $action service: $output"
]);
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}