Extraction du code self-test dans des fichiers partagés (selftest.js + selftest-modal.html) pour éviter la duplication. Ajout du bouton Run Self Test sur les pages index, sensors et admin. Nouveau test RTC qui vérifie la connexion du module DS3231 et la synchronisation horloge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1953 lines
74 KiB
HTML
Executable File
1953 lines
74 KiB
HTML
Executable File
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>NebuleAir</title>
|
||
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||
<style>
|
||
body {
|
||
overflow-x: hidden;
|
||
}
|
||
#sidebar a.nav-link {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
#sidebar a.nav-link:hover {
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
}
|
||
#sidebar a.nav-link svg {
|
||
margin-right: 8px; /* Add spacing between icons and text */
|
||
}
|
||
#sidebar {
|
||
transition: transform 0.3s ease-in-out;
|
||
}
|
||
.offcanvas-backdrop {
|
||
z-index: 1040;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Topbar -->
|
||
<span id="topbar"></span>
|
||
|
||
<!-- Sidebar Offcanvas for Mobile -->
|
||
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel">
|
||
<div class="offcanvas-header">
|
||
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||
</div>
|
||
<div class="offcanvas-body" id="sidebar_mobile">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container-fluid mt-5">
|
||
<div class="row">
|
||
<!-- Side bar -->
|
||
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
||
</aside>
|
||
<!-- Main content -->
|
||
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
||
<h1 class="mt-4">Admin</h1>
|
||
|
||
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
|
||
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
|
||
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
|
||
</svg>
|
||
Run Self Test
|
||
</button>
|
||
|
||
<div class="row mb-3">
|
||
|
||
<div class="col-lg-3 col-12">
|
||
<h3 class="mt-4">Parameters (config)</h3>
|
||
|
||
<form>
|
||
|
||
<div class="mb-3">
|
||
<label for="device_name" class="form-label">Device Name</label>
|
||
<input type="text" class="form-control" id="device_name" onchange="update_config_sqlite('deviceName', this.value)">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="device_ID" class="form-label">Device ID</label>
|
||
<input type="text" class="form-control" id="device_ID" disabled>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="modem_version" class="form-label">Modem Version</label>
|
||
<input type="text" class="form-control" id="modem_version" disabled>
|
||
</div>
|
||
|
||
<!-- config_scripts_table -->
|
||
|
||
<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">
|
||
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_sqlite('BME280', this.checked)">
|
||
<label class="form-check-label" for="check_bme280">
|
||
Send temp/hum data (BME280)
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-check mb-3">
|
||
<input class="form-check-input" type="checkbox" value="" id="check_envea" onchange="update_config_sqlite('envea', this.checked);add_sondeEnveaContainer() ">
|
||
<label class="form-check-label" for="check_envea">
|
||
Send Envea sensor data
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-check mb-3">
|
||
<input class="form-check-input" type="checkbox" value="" id="check_solarBattery" onchange="update_config_sqlite('MPPT', this.checked)">
|
||
<label class="form-check-label" for="check_solarBattery">
|
||
Send Solar / Battery MPPT data
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-check mb-3">
|
||
<input class="form-check-input" type="checkbox" value="" id="check_WindMeter" onchange="update_config_sqlite('windMeter', this.checked)">
|
||
<label class="form-check-label" for="check_WindMeter">
|
||
Send Wind Meter data
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-check mb-3">
|
||
<input class="form-check-input" type="checkbox" value="" id="check_NOISE" onchange="update_config_sqlite('NOISE', this.checked)">
|
||
<label class="form-check-label" for="check_NOISE">
|
||
Send Noise data
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-check mb-3">
|
||
<input class="form-check-input" type="checkbox" value="" id="check_mhz19" onchange="update_config_sqlite('MHZ19', this.checked)">
|
||
<label class="form-check-label" for="check_mhz19">
|
||
Send CO2 data (MH-Z19)
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-check mb-3">
|
||
<input class="form-check-input" type="checkbox" value="" id="check_wifi_power_saving" onchange="update_config_sqlite('wifi_power_saving', this.checked)">
|
||
<label class="form-check-label" for="check_wifi_power_saving">
|
||
WiFi Power Saving
|
||
</label>
|
||
<small class="form-text text-muted d-block ms-4">
|
||
Disable WiFi 10 minutes after boot to save power (~100-200mA). WiFi will re-enable after reboot.
|
||
</small>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="cpu_power_mode" class="form-label">CPU Power Mode</label>
|
||
<select class="form-select" id="cpu_power_mode" onchange="set_cpu_power_mode(this.value)">
|
||
<option value="normal">Normal (600-1500MHz dynamic)</option>
|
||
<option value="powersave">Power Saving (600MHz fixed)</option>
|
||
</select>
|
||
<small class="form-text text-muted d-block">
|
||
<span id="cpu_mode_status" class="text-success"></span>
|
||
</small>
|
||
<small class="form-text text-muted d-block">
|
||
Power saving mode reduces CPU performance by ~30-40% but saves power.
|
||
</small>
|
||
</div>
|
||
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<span class="fw-bold">Protected Settings</span>
|
||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="toggleProtectedSettings()" id="unlockBtn">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock-fill" viewBox="0 0 16 16">
|
||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||
</svg>
|
||
Unlock
|
||
</button>
|
||
</div>
|
||
|
||
<div class="form-check mb-3">
|
||
<input class="form-check-input protected-checkbox" type="checkbox" value="" id="check_aircarto" onchange="update_config_sqlite('send_aircarto', this.checked)" disabled>
|
||
<label class="form-check-label" for="check_aircarto">
|
||
Send to AirCarto (HTTP)
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-check mb-3">
|
||
<input class="form-check-input protected-checkbox" 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 (HTTPS)
|
||
</label>
|
||
</div>
|
||
|
||
|
||
<div class="form-check mb-3">
|
||
<input class="form-check-input protected-checkbox" type="checkbox" value="" id="check_miotiq" onchange="update_config_sqlite('send_miotiq', this.checked)" disabled>
|
||
<label class="form-check-label" for="check_miotiq">
|
||
Send to miotiq (UDP)
|
||
</label>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="device_type" class="form-label">Device Type</label>
|
||
<select class="form-select protected-checkbox" id="device_type" onchange="update_config_sqlite('device_type', this.value)" disabled>
|
||
<option value="nebuleair_pro">NebuleAir Pro</option>
|
||
<option value="moduleair_pro">ModuleAir Pro</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="input-group mb-3" id="sondes_envea_div"></div>
|
||
|
||
<div id="envea_table"></div>
|
||
|
||
|
||
<!--<button type="submit" class="btn btn-primary">Submit</button>-->
|
||
</form>
|
||
</div>
|
||
|
||
<!-- CLOCK-->
|
||
<div class="col-lg-3 col-12">
|
||
|
||
<h3 class="mt-4">Clock</h3>
|
||
|
||
<div class="mb-3">
|
||
<label for="sys_local_time" class="form-label">System time (local)</label>
|
||
<input type="text" class="form-control" id="sys_local_time" disabled>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="sys_UTC_time" class="form-label">System time (UTC)</label>
|
||
<input type="text" class="form-control" id="sys_UTC_time" disabled>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="RTC_utc_time" class="form-label">RTC time (UTC)</label>
|
||
<input type="text" class="form-control" id="RTC_utc_time" disabled>
|
||
</div>
|
||
|
||
|
||
<div id="alert_container"></div>
|
||
|
||
<h5 class="mt-4">Set RTC</h5>
|
||
|
||
<button type="submit" class="btn btn-primary mb-1" onclick="set_RTC_withNTP()">WiFi (NTP) </button>
|
||
<button type="submit" class="btn btn-primary mb-1" onclick="set_RTC_withBrowser()">Browser time </button>
|
||
<button type="submit" class="btn btn-primary mb-1" onclick="set_RTC_with4G()" disabled>4G (NTP) </button>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- UPDATE-->
|
||
|
||
<div class="col-lg-4 col-12">
|
||
<div class="d-flex align-items-center mt-4 mb-2">
|
||
<h3 class="mb-0 me-2">Updates</h3>
|
||
<span id="firmwareVersionBadge" class="badge bg-secondary">Version...</span>
|
||
<button type="button" class="btn btn-sm btn-outline-info ms-2" onclick="showChangelogModal()">Changelog</button>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary" onclick="updateFirmware()" id="updateBtn">
|
||
<span id="updateBtnText">Update firmware</span>
|
||
<span id="updateSpinner" class="spinner-border spinner-border-sm ms-2" style="display: none;"></span>
|
||
</button>
|
||
|
||
<hr class="my-3">
|
||
<label class="form-label fw-bold">Mise à jour hors-ligne (upload)</label>
|
||
<div class="input-group mb-2">
|
||
<input type="file" class="form-control" id="firmwareFileInput" accept=".zip">
|
||
<button class="btn btn-warning" type="button" onclick="uploadFirmware()" id="uploadBtn">
|
||
<span id="uploadBtnText">Upload & Install</span>
|
||
<span id="uploadSpinner" class="spinner-border spinner-border-sm ms-2" style="display: none;"></span>
|
||
</button>
|
||
</div>
|
||
<div class="progress mb-2" id="uploadProgressBar" style="display: none; height: 20px;">
|
||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%" id="uploadProgress">0%</div>
|
||
</div>
|
||
<small class="text-muted">Télécharger le .zip depuis Gitea, puis le déposer ici</small>
|
||
|
||
<!-- Update Output Console -->
|
||
<div id="updateOutput" class="mt-3" style="display: none;">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span class="fw-bold">Update Log</span>
|
||
<div>
|
||
<button type="button" class="btn btn-sm btn-success me-2" onclick="location.reload()" id="reloadBtn" style="display: none;">
|
||
<svg width="14" height="14" fill="currentColor" class="bi bi-arrow-clockwise me-1" 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>
|
||
Reload Page
|
||
</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearUpdateOutput()">
|
||
Clear
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<pre id="updateOutputContent" class="mb-0" style="max-height: 400px; overflow-y: auto; font-size: 0.85rem; background-color: #f8f9fa; padding: 1rem; border-radius: 0.375rem;"></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</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 -->
|
||
|
||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||
<div id="liveToast" class="toast align-items-center text-bg-primary border-1" role="alert" aria-live="assertive" aria-atomic="true">
|
||
<div class="d-flex">
|
||
<div class="toast-body">
|
||
Hello, world! This is a toast message.
|
||
</div>
|
||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Envea Detection Modal -->
|
||
<div class="modal fade" id="enveaDetectionModal" tabindex="-1" aria-labelledby="enveaDetectionModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-xl">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="enveaDetectionModalLabel">Envea Sondes Detection</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
|
||
<div id="detectionProgress" class="text-center" style="display: none;">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Loading...</span>
|
||
</div>
|
||
<p class="mt-2">Scanning ports for Envea devices...</p>
|
||
</div>
|
||
<div id="detectionResults">
|
||
<p>Click "Start Detection" to scan for connected Envea devices.</p>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||
<button type="button" class="btn btn-primary" id="startDetectionBtn" onclick="startEnveaDetection()">Start Detection</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- Changelog Modal -->
|
||
<div class="modal fade" id="changelogModal" tabindex="-1" aria-labelledby="changelogModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="changelogModalLabel">Changelog</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body" id="changelogModalBody">
|
||
<div class="text-center">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Loading...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- JAVASCRIPT -->
|
||
|
||
<!-- Link Ajax locally -->
|
||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||
<!-- Link Bootstrap JS and Popper.js locally -->
|
||
<script src="assets/js/bootstrap.bundle.js"></script>
|
||
<!-- i18n translation system -->
|
||
<script src="assets/js/i18n.js"></script>
|
||
<script src="assets/js/topbar-logo.js"></script>
|
||
<script src="assets/js/selftest.js"></script>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
const elementsToLoad = [
|
||
{ id: 'topbar', file: 'topbar.html' },
|
||
{ id: 'sidebar', file: 'sidebar.html' },
|
||
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
||
];
|
||
|
||
elementsToLoad.forEach(({ id, file }) => {
|
||
fetch(file)
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
const element = document.getElementById(id);
|
||
if (element) {
|
||
element.innerHTML = data;
|
||
}
|
||
})
|
||
.catch(error => console.error(`Error loading ${file}:`, error));
|
||
});
|
||
});
|
||
|
||
//end document.addEventListener
|
||
|
||
|
||
/*
|
||
___ _ _
|
||
/ _ \ _ __ | | ___ __ _ __| |
|
||
| | | | '_ \| | / _ \ / _` |/ _` |
|
||
| |_| | | | | |__| (_) | (_| | (_| |
|
||
\___/|_| |_|_____\___/ \__,_|\__,_|
|
||
|
||
*/
|
||
|
||
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 name html page title
|
||
if (response.deviceName) {
|
||
document.title = response.deviceName;
|
||
}
|
||
//device ID
|
||
const deviceID = response.deviceID.trim().toUpperCase();
|
||
const device_ID = document.getElementById("device_ID");
|
||
device_ID.value = response.deviceID.toUpperCase();
|
||
//modem_version
|
||
const modem_version = document.getElementById("modem_version");
|
||
modem_version.value = response.modem_version;
|
||
|
||
|
||
|
||
const checkbox_nmp5channels = document.getElementById("check_NPM_5channels");
|
||
const checkbox_wind = document.getElementById("check_WindMeter");
|
||
const checkbox_uSpot = document.getElementById("check_uSpot");
|
||
const checkbox_aircarto = document.getElementById("check_aircarto");
|
||
const checkbox_miotiq = document.getElementById("check_miotiq");
|
||
|
||
const checkbox_bme = document.getElementById("check_bme280");
|
||
const checkbox_envea = document.getElementById("check_envea");
|
||
const checkbox_solar = document.getElementById("check_solarBattery");
|
||
const checkbox_noise = document.getElementById("check_NOISE");
|
||
const checkbox_mhz19 = document.getElementById("check_mhz19");
|
||
const checkbox_wifi_power_saving = document.getElementById("check_wifi_power_saving");
|
||
|
||
checkbox_bme.checked = response["BME280"];
|
||
checkbox_envea.checked = response["envea"];
|
||
checkbox_solar.checked = response["MPPT"];
|
||
checkbox_nmp5channels.checked = response.npm_5channel;
|
||
checkbox_wind.checked = response["windMeter"];
|
||
checkbox_noise.checked = response["NOISE"];
|
||
checkbox_mhz19.checked = response["MHZ19"];
|
||
checkbox_wifi_power_saving.checked = response["wifi_power_saving"];
|
||
|
||
checkbox_uSpot.checked = response["send_uSpot"];
|
||
checkbox_aircarto.checked = response["send_aircarto"];
|
||
checkbox_miotiq.checked = response["send_miotiq"];
|
||
|
||
// Set device type
|
||
const device_type_select = document.getElementById("device_type");
|
||
if (response["device_type"]) {
|
||
device_type_select.value = response["device_type"];
|
||
}
|
||
|
||
// Set CPU power mode
|
||
const cpu_power_mode_select = document.getElementById("cpu_power_mode");
|
||
if (response["cpu_power_mode"]) {
|
||
cpu_power_mode_select.value = response["cpu_power_mode"];
|
||
// Update status display
|
||
const statusElement = document.getElementById('cpu_mode_status');
|
||
statusElement.textContent = `Current: ${response["cpu_power_mode"]}`;
|
||
statusElement.className = 'text-success';
|
||
}
|
||
|
||
// If envea is enabled, show the envea sondes container
|
||
if (response["envea"]) {
|
||
add_sondeEnveaContainer();
|
||
}
|
||
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error('AJAX request failed:', status, error);
|
||
}
|
||
});//end AJAX
|
||
|
||
|
||
|
||
|
||
//OLD way to get config (JSON)
|
||
/*
|
||
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
||
.then(response => response.json()) // Parse response as JSON
|
||
.then(data => {
|
||
console.log("Getting config file (onload)");
|
||
//get device ID
|
||
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
||
//get device Name
|
||
//const deviceName = data.deviceName;
|
||
|
||
//get BME check
|
||
const checkbox = document.getElementById("check_bme280");
|
||
checkbox.checked = data["BME280/get_data_v2.py"];
|
||
|
||
//get NPM-5channels check
|
||
const checkbox_NPM_5channels = document.getElementById("check_NPM_5channels");
|
||
checkbox_NPM_5channels.checked = data["NextPM_5channels"];
|
||
|
||
//get sonde Envea check
|
||
const checkbox_envea = document.getElementById("check_envea");
|
||
checkbox_envea.checked = data["envea/read_value_v2.py"];
|
||
|
||
//device name
|
||
//const device_name = document.getElementById("device_name");
|
||
//device_name.value = data.deviceName;
|
||
|
||
|
||
})
|
||
.catch(error => console.error('Error loading config.json:', error));
|
||
*/
|
||
|
||
|
||
|
||
//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("Getting RTC times");
|
||
|
||
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 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();
|
||
|
||
// Load firmware version
|
||
loadFirmwareVersion();
|
||
|
||
} //end window.onload
|
||
|
||
|
||
|
||
|
||
|
||
function update_config_sqlite(param, value){
|
||
console.log("Updating sqlite ",param," : ", value);
|
||
const toastLiveExample = document.getElementById('liveToast')
|
||
const toastBody = toastLiveExample.querySelector('.toast-body');
|
||
$.ajax({
|
||
url: 'launcher.php?type=update_config_sqlite¶m='+param+'&value='+value,
|
||
dataType: 'json', // 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);
|
||
// 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.param || param}<br>
|
||
Value: ${response.value || checked}<br>
|
||
${response.message || ''}
|
||
`;
|
||
} 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.param || 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 set_cpu_power_mode(mode) {
|
||
console.log("Setting CPU power mode to:", mode);
|
||
|
||
const toastLiveExample = document.getElementById('liveToast');
|
||
const toastBody = toastLiveExample.querySelector('.toast-body');
|
||
const statusElement = document.getElementById('cpu_mode_status');
|
||
|
||
// Show loading status
|
||
statusElement.textContent = 'Applying mode...';
|
||
statusElement.className = 'text-warning';
|
||
|
||
$.ajax({
|
||
url: 'launcher.php?type=set_cpu_power_mode&mode=' + mode,
|
||
dataType: 'json',
|
||
method: 'GET',
|
||
cache: false,
|
||
success: function(response) {
|
||
console.log(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>
|
||
CPU mode set to: <strong>${mode}</strong><br>
|
||
${response.description || ''}
|
||
`;
|
||
|
||
// Update status
|
||
statusElement.textContent = `Current: ${mode}`;
|
||
statusElement.className = 'text-success';
|
||
|
||
} else {
|
||
// Error message
|
||
toastLiveExample.classList.remove('text-bg-success');
|
||
toastLiveExample.classList.add('text-bg-danger');
|
||
|
||
formattedMessage = `
|
||
<strong>Error!</strong><br>
|
||
${response.error || 'Failed to set CPU power mode'}
|
||
`;
|
||
|
||
// Reset status
|
||
statusElement.textContent = 'Error setting mode';
|
||
statusElement.className = 'text-danger';
|
||
}
|
||
|
||
// 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);
|
||
|
||
// Show error in toast
|
||
toastLiveExample.classList.remove('text-bg-success');
|
||
toastLiveExample.classList.add('text-bg-danger');
|
||
toastBody.innerHTML = `<strong>Error!</strong><br>Network error: ${error}`;
|
||
|
||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||
toastBootstrap.show();
|
||
|
||
// Update status
|
||
statusElement.textContent = 'Network error';
|
||
statusElement.className = 'text-danger';
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
function update_config(param, value){
|
||
console.log("Updating ",param," : ", value);
|
||
$.ajax({
|
||
url: 'launcher.php?type=update_config¶m='+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 updateFirmware() {
|
||
console.log("Starting comprehensive firmware update...");
|
||
|
||
// Show loading state
|
||
const updateBtn = document.getElementById('updateBtn');
|
||
const updateBtnText = document.getElementById('updateBtnText');
|
||
const updateSpinner = document.getElementById('updateSpinner');
|
||
const updateOutput = document.getElementById('updateOutput');
|
||
const updateOutputContent = document.getElementById('updateOutputContent');
|
||
|
||
// Disable button and show spinner
|
||
updateBtn.disabled = true;
|
||
updateBtnText.textContent = 'Updating...';
|
||
updateSpinner.style.display = 'inline-block';
|
||
|
||
// Show output console
|
||
updateOutput.style.display = 'block';
|
||
updateOutputContent.textContent = 'Starting update process...\n';
|
||
|
||
$.ajax({
|
||
url: 'launcher.php?type=update_firmware',
|
||
method: 'GET',
|
||
dataType: 'json',
|
||
timeout: 120000, // 2 minutes timeout
|
||
|
||
success: function(response) {
|
||
console.log('Update completed:', response);
|
||
|
||
// Display formatted output
|
||
if (response.success && response.output) {
|
||
// Format the output for better readability
|
||
const formattedOutput = response.output
|
||
.replace(/\[\d{2}:\d{2}:\d{2}\]/g, function(match) {
|
||
return `<span style="color: #007bff; font-weight: bold;">${match}</span>`;
|
||
})
|
||
.replace(/✓/g, '<span style="color: #28a745;">✓</span>')
|
||
.replace(/✗/g, '<span style="color: #dc3545;">✗</span>')
|
||
.replace(/⚠/g, '<span style="color: #ffc107;">⚠</span>')
|
||
.replace(/ℹ/g, '<span style="color: #17a2b8;">ℹ</span>');
|
||
|
||
updateOutputContent.innerHTML = formattedOutput;
|
||
|
||
// Show success toast and reload button
|
||
showToast('Update completed successfully!', 'success');
|
||
document.getElementById('reloadBtn').style.display = 'inline-block';
|
||
} else {
|
||
updateOutputContent.textContent = 'Update completed but no output received.';
|
||
showToast('Update may have completed with issues', 'warning');
|
||
}
|
||
},
|
||
|
||
error: function(xhr, status, error) {
|
||
console.error('Update failed:', status, error);
|
||
updateOutputContent.textContent = `Update failed: ${error}\n\nStatus: ${status}\nResponse: ${xhr.responseText || 'No response'}`;
|
||
showToast('Update failed! Check the output for details.', 'error');
|
||
},
|
||
|
||
complete: function() {
|
||
// Reset button state
|
||
updateBtn.disabled = false;
|
||
updateBtnText.textContent = 'Update firmware';
|
||
updateSpinner.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
function uploadFirmware() {
|
||
const fileInput = document.getElementById('firmwareFileInput');
|
||
const file = fileInput.files[0];
|
||
|
||
if (!file) {
|
||
showToast('Please select a .zip file first', 'warning');
|
||
return;
|
||
}
|
||
|
||
// Validate extension
|
||
if (!file.name.toLowerCase().endsWith('.zip')) {
|
||
showToast('Only .zip files are allowed', 'error');
|
||
return;
|
||
}
|
||
|
||
// Validate size (50MB)
|
||
if (file.size > 50 * 1024 * 1024) {
|
||
showToast('File too large (max 50MB)', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!confirm('Install firmware from "' + file.name + '"?\nThis will update the system files and restart services.')) {
|
||
return;
|
||
}
|
||
|
||
// UI elements
|
||
const uploadBtn = document.getElementById('uploadBtn');
|
||
const uploadBtnText = document.getElementById('uploadBtnText');
|
||
const uploadSpinner = document.getElementById('uploadSpinner');
|
||
const progressBar = document.getElementById('uploadProgressBar');
|
||
const progress = document.getElementById('uploadProgress');
|
||
const updateOutput = document.getElementById('updateOutput');
|
||
const updateOutputContent = document.getElementById('updateOutputContent');
|
||
|
||
// Show loading state
|
||
uploadBtn.disabled = true;
|
||
uploadBtnText.textContent = 'Uploading...';
|
||
uploadSpinner.style.display = 'inline-block';
|
||
progressBar.style.display = 'flex';
|
||
progress.style.width = '0%';
|
||
progress.textContent = '0%';
|
||
updateOutput.style.display = 'block';
|
||
updateOutputContent.textContent = 'Uploading firmware file...\n';
|
||
|
||
// Build FormData
|
||
const formData = new FormData();
|
||
formData.append('firmware_file', file);
|
||
|
||
// Use XMLHttpRequest for upload progress
|
||
const xhr = new XMLHttpRequest();
|
||
xhr.timeout = 300000; // 5 minutes
|
||
|
||
xhr.upload.addEventListener('progress', function(e) {
|
||
if (e.lengthComputable) {
|
||
const pct = Math.round((e.loaded / e.total) * 100);
|
||
progress.style.width = pct + '%';
|
||
progress.textContent = pct + '%';
|
||
if (pct >= 100) {
|
||
uploadBtnText.textContent = 'Installing...';
|
||
updateOutputContent.textContent = 'Upload complete. Installing firmware...\n';
|
||
}
|
||
}
|
||
});
|
||
|
||
xhr.addEventListener('load', function() {
|
||
try {
|
||
const response = JSON.parse(xhr.responseText);
|
||
if (response.success && response.output) {
|
||
const formattedOutput = response.output
|
||
.replace(/✓/g, '<span style="color: #28a745;">✓</span>')
|
||
.replace(/✗/g, '<span style="color: #dc3545;">✗</span>')
|
||
.replace(/⚠/g, '<span style="color: #ffc107;">⚠</span>')
|
||
.replace(/ℹ/g, '<span style="color: #17a2b8;">ℹ</span>');
|
||
updateOutputContent.innerHTML = formattedOutput;
|
||
showToast('Firmware updated: ' + (response.old_version || '?') + ' → ' + (response.new_version || '?'), 'success');
|
||
document.getElementById('reloadBtn').style.display = 'inline-block';
|
||
} else {
|
||
updateOutputContent.textContent = 'Error: ' + (response.message || 'Unknown error');
|
||
showToast('Upload failed: ' + (response.message || 'Unknown error'), 'error');
|
||
}
|
||
} catch (e) {
|
||
updateOutputContent.textContent = 'Error parsing response: ' + xhr.responseText;
|
||
showToast('Update failed: invalid server response', 'error');
|
||
}
|
||
resetUploadUI();
|
||
});
|
||
|
||
xhr.addEventListener('error', function() {
|
||
updateOutputContent.textContent = 'Network error during upload';
|
||
showToast('Upload failed: network error', 'error');
|
||
resetUploadUI();
|
||
});
|
||
|
||
xhr.addEventListener('timeout', function() {
|
||
updateOutputContent.textContent = 'Upload timed out (5 min limit)';
|
||
showToast('Upload timed out', 'error');
|
||
resetUploadUI();
|
||
});
|
||
|
||
function resetUploadUI() {
|
||
uploadBtn.disabled = false;
|
||
uploadBtnText.textContent = 'Upload & Install';
|
||
uploadSpinner.style.display = 'none';
|
||
progressBar.style.display = 'none';
|
||
}
|
||
|
||
xhr.open('POST', 'launcher.php?type=upload_firmware');
|
||
xhr.send(formData);
|
||
}
|
||
|
||
function clearUpdateOutput() {
|
||
const updateOutput = document.getElementById('updateOutput');
|
||
const updateOutputContent = document.getElementById('updateOutputContent');
|
||
const reloadBtn = document.getElementById('reloadBtn');
|
||
|
||
updateOutputContent.textContent = '';
|
||
updateOutput.style.display = 'none';
|
||
reloadBtn.style.display = 'none';
|
||
}
|
||
|
||
function showToast(message, type) {
|
||
const toastLiveExample = document.getElementById('liveToast');
|
||
const toastBody = toastLiveExample.querySelector('.toast-body');
|
||
|
||
// Set toast color based on type
|
||
toastLiveExample.classList.remove('text-bg-primary', 'text-bg-success', 'text-bg-danger', 'text-bg-warning');
|
||
switch(type) {
|
||
case 'success':
|
||
toastLiveExample.classList.add('text-bg-success');
|
||
break;
|
||
case 'error':
|
||
toastLiveExample.classList.add('text-bg-danger');
|
||
break;
|
||
case 'warning':
|
||
toastLiveExample.classList.add('text-bg-warning');
|
||
break;
|
||
default:
|
||
toastLiveExample.classList.add('text-bg-primary');
|
||
}
|
||
|
||
toastBody.textContent = message;
|
||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||
toastBootstrap.show();
|
||
}
|
||
|
||
// Legacy function for backward compatibility
|
||
function updateGitPull() {
|
||
updateFirmware();
|
||
}
|
||
|
||
function set_RTC_withNTP(){
|
||
console.log("Set RTC module with WIFI (NTP server)");
|
||
|
||
$.ajax({
|
||
url: 'launcher.php?type=set_RTC_withNTP',
|
||
method: 'GET', // Use GET or POST depending on your needs
|
||
dataType: 'text', // Specify that you expect a JSON response
|
||
|
||
success: function(response) {
|
||
// Handle success response if needed
|
||
console.log(response);
|
||
alert(response);
|
||
// Reload the page after the device update
|
||
location.reload(); // This will reload the page
|
||
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error('AJAX request failed:', status, error);
|
||
}
|
||
}); //end ajax
|
||
}
|
||
|
||
function set_RTC_withBrowser(){
|
||
console.log("Set RTC module with browser time");
|
||
const browserTime = new Date(); // Get the current time in the browser
|
||
const formattedTime = browserTime.toISOString(); // Convert to ISO string (UTC)
|
||
console.log(formattedTime);
|
||
|
||
$.ajax({
|
||
url: `launcher.php?type=set_RTC_withBrowser&time=${encodeURIComponent(formattedTime)}`,
|
||
method: 'GET', // Use GET or POST depending on your needs
|
||
dataType: 'json', // Specify that you expect a JSON response
|
||
|
||
success: function(response) {
|
||
// Handle success response if needed
|
||
console.log(response);
|
||
if (response.success) {
|
||
alert("RTC successfully updated!");
|
||
} else {
|
||
alert(`Error: ${response.message}`);
|
||
} // Reload the page after the device update
|
||
location.reload(); // This will reload the page
|
||
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error('AJAX request failed:', status, error);
|
||
}
|
||
}); //end ajax
|
||
}
|
||
|
||
/*
|
||
____ _ _____
|
||
/ ___| ___ _ __ __| | ___ ___ | ____|_ ____ _____ __ _
|
||
\___ \ / _ \| '_ \ / _` |/ _ \/ __| | _| | '_ \ \ / / _ \/ _` |
|
||
___) | (_) | | | | (_| | __/\__ \ | |___| | | \ V / __/ (_| |
|
||
|____/ \___/|_| |_|\__,_|\___||___/ |_____|_| |_|\_/ \___|\__,_|
|
||
|
||
*/
|
||
|
||
function add_sondeEnveaContainer() {
|
||
console.log("Sonde Envea is true: need to add container!");
|
||
|
||
// Getting envea_sondes_table data
|
||
$.ajax({
|
||
url: 'launcher.php?type=get_envea_sondes_table_sqlite',
|
||
dataType: 'json',
|
||
method: 'GET',
|
||
success: function(sondes) {
|
||
console.log("Getting SQLite envea sondes table:");
|
||
console.log(sondes);
|
||
|
||
// Create container div if it doesn't exist
|
||
if ($('#sondes_envea_div').length === 0) {
|
||
$('#advanced_options').append('<div id="sondes_envea_div" class="input-group mt-4 border p-3 rounded"><legend>Sondes Envea</legend><p>Plouf</p></div>');
|
||
} else {
|
||
// Clear existing content if container exists
|
||
$('#sondes_envea_div').html('<legend>Sondes Envea <button type="button" class="btn btn-sm btn-info ms-2" onclick="detectEnveaSondes()">Detect Devices</button></legend>');
|
||
$('#envea_table').html('<table class="table table-striped table-bordered">'+
|
||
'<thead><tr><th scope="col">Software</th><th scope="col">Hardware (PCB)</th></tr></thead>'+
|
||
'<tbody>' +
|
||
'<tr><td>ttyAMA5</td><td>NPM1</td></tr>' +
|
||
'<tr><td>ttyAMA4</td><td>NPM2</td></tr>' +
|
||
'<tr><td>ttyAMA3</td><td>NPM3</td></tr>' +
|
||
'<tr><td>ttyAMA2</td><td>SARA</td></tr>' +
|
||
'</tbody></table>');
|
||
}
|
||
|
||
// Loop through each sonde and create UI elements
|
||
sondes.forEach(function(sonde) {
|
||
// Create a unique ID for this sonde
|
||
const sondeId = `sonde_${sonde.id}`;
|
||
|
||
// Create HTML for this sonde
|
||
const sondeHtml = `
|
||
<div class="input-group mb-3" id="${sondeId}_container">
|
||
<div class="input-group-text">
|
||
<input class="form-check-input mt-0" type="checkbox" id="${sondeId}_enabled"
|
||
${sonde.connected ? 'checked' : ''}
|
||
onchange="updateSondeStatus(${sonde.id}, this.checked)">
|
||
</div>
|
||
<input type="text" class="form-control" placeholder="Name" value="${sonde.name}"
|
||
id="${sondeId}_name" readonly style="background-color: #f8f9fa;">
|
||
<select class="form-control" id="${sondeId}_port" onchange="updateSondePort(${sonde.id}, this.value)">
|
||
<option value="ttyAMA3" ${sonde.port === 'ttyAMA3' ? 'selected' : ''}>ttyAMA3</option>
|
||
<option value="ttyAMA4" ${sonde.port === 'ttyAMA4' ? 'selected' : ''}>ttyAMA4</option>
|
||
<option value="ttyAMA5" ${sonde.port === 'ttyAMA5' ? 'selected' : ''}>ttyAMA5</option>
|
||
</select>
|
||
<input type="number" class="form-control" placeholder="Coefficient" value="${sonde.coefficient}"
|
||
id="${sondeId}_coefficient" onchange="updateSondeCoefficientWithConfirm(${sonde.id}, this.value, this)">
|
||
</div>
|
||
`;
|
||
|
||
// Append this sonde to the container
|
||
$('#sondes_envea_div').append(sondeHtml);
|
||
});
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error('AJAX request failed:', status, error);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Helper functions for updating sonde properties
|
||
function updateSondeStatus(id, connected) {
|
||
console.log(`Updating sonde ${id} connected status to: ${connected}`);
|
||
const toastLiveExample = document.getElementById('liveToast');
|
||
const toastBody = toastLiveExample.querySelector('.toast-body');
|
||
|
||
$.ajax({
|
||
url: `launcher.php?type=update_sonde&id=${id}&field=connected&value=${connected ? 1 : 0}`,
|
||
dataType: 'json',
|
||
method: 'GET',
|
||
cache: false,
|
||
success: function(response) {
|
||
console.log('Sonde status updated:', response);
|
||
|
||
// Format the response for toast
|
||
let formattedMessage = '';
|
||
|
||
if (response.success) {
|
||
// Success message
|
||
toastLiveExample.classList.remove('text-bg-danger');
|
||
toastLiveExample.classList.add('text-bg-success');
|
||
|
||
formattedMessage = `
|
||
<strong>Success!</strong><br>
|
||
Sonde ID: ${response.id}<br>
|
||
Connected: ${connected ? "Yes" : "No"}<br>
|
||
${response.message || ''}
|
||
`;
|
||
} 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>
|
||
Sonde ID: ${id}
|
||
`;
|
||
}
|
||
|
||
// Update and show toast
|
||
toastBody.innerHTML = formattedMessage;
|
||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||
toastBootstrap.show();
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error('Failed to update sonde status:', error);
|
||
|
||
// Show error toast
|
||
toastLiveExample.classList.remove('text-bg-success');
|
||
toastLiveExample.classList.add('text-bg-danger');
|
||
toastBody.innerHTML = `
|
||
<strong>Request Failed!</strong><br>
|
||
Error: ${error}<br>
|
||
Sonde ID: ${id}
|
||
`;
|
||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||
toastBootstrap.show();
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateSondeName(id, name) {
|
||
console.log(`Updating sonde ${id} name to: ${name}`);
|
||
const toastLiveExample = document.getElementById('liveToast');
|
||
const toastBody = toastLiveExample.querySelector('.toast-body');
|
||
|
||
$.ajax({
|
||
url: `launcher.php?type=update_sonde&id=${id}&field=name&value=${encodeURIComponent(name)}`,
|
||
dataType: 'json',
|
||
method: 'GET',
|
||
cache: false,
|
||
success: function(response) {
|
||
console.log('Sonde name updated:', response);
|
||
|
||
// Format the response for toast
|
||
let formattedMessage = '';
|
||
|
||
if (response.success) {
|
||
// Success message
|
||
toastLiveExample.classList.remove('text-bg-danger');
|
||
toastLiveExample.classList.add('text-bg-success');
|
||
|
||
formattedMessage = `
|
||
<strong>Success!</strong><br>
|
||
Sonde ID: ${response.id}<br>
|
||
Name: ${name}<br>
|
||
${response.message || ''}
|
||
`;
|
||
} 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>
|
||
Sonde ID: ${id}
|
||
`;
|
||
}
|
||
|
||
// Update and show toast
|
||
toastBody.innerHTML = formattedMessage;
|
||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||
toastBootstrap.show();
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error('Failed to update sonde name:', error);
|
||
|
||
// Show error toast
|
||
toastLiveExample.classList.remove('text-bg-success');
|
||
toastLiveExample.classList.add('text-bg-danger');
|
||
toastBody.innerHTML = `
|
||
<strong>Request Failed!</strong><br>
|
||
Error: ${error}<br>
|
||
Sonde ID: ${id}
|
||
`;
|
||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||
toastBootstrap.show();
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateSondePort(id, port) {
|
||
console.log(`Updating sonde ${id} port to: ${port}`);
|
||
const toastLiveExample = document.getElementById('liveToast');
|
||
const toastBody = toastLiveExample.querySelector('.toast-body');
|
||
|
||
$.ajax({
|
||
url: `launcher.php?type=update_sonde&id=${id}&field=port&value=${encodeURIComponent(port)}`,
|
||
dataType: 'json',
|
||
method: 'GET',
|
||
cache: false,
|
||
success: function(response) {
|
||
console.log('Sonde port updated:', response);
|
||
|
||
// Format the response for toast
|
||
let formattedMessage = '';
|
||
|
||
if (response.success) {
|
||
// Success message
|
||
toastLiveExample.classList.remove('text-bg-danger');
|
||
toastLiveExample.classList.add('text-bg-success');
|
||
|
||
formattedMessage = `
|
||
<strong>Success!</strong><br>
|
||
Sonde ID: ${response.id}<br>
|
||
Port: ${port}<br>
|
||
${response.message || ''}
|
||
`;
|
||
} 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>
|
||
Sonde ID: ${id}
|
||
`;
|
||
}
|
||
|
||
// Update and show toast
|
||
toastBody.innerHTML = formattedMessage;
|
||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||
toastBootstrap.show();
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error('Failed to update sonde port:', error);
|
||
|
||
// Show error toast
|
||
toastLiveExample.classList.remove('text-bg-success');
|
||
toastLiveExample.classList.add('text-bg-danger');
|
||
toastBody.innerHTML = `
|
||
<strong>Request Failed!</strong><br>
|
||
Error: ${error}<br>
|
||
Sonde ID: ${id}
|
||
`;
|
||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||
toastBootstrap.show();
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateSondeCoefficientWithConfirm(id, coefficient, inputElement) {
|
||
// Store the previous value in case user cancels
|
||
const previousValue = inputElement.getAttribute('data-previous-value') || inputElement.defaultValue;
|
||
|
||
// Show confirmation dialog
|
||
const confirmed = confirm(`Are you sure you want to change the coefficient to ${coefficient}?\n\nThis will affect sensor calibration and data accuracy.`);
|
||
|
||
if (confirmed) {
|
||
// Store the new value as previous for next time
|
||
inputElement.setAttribute('data-previous-value', coefficient);
|
||
updateSondeCoefficient(id, coefficient);
|
||
} else {
|
||
// Revert to previous value
|
||
inputElement.value = previousValue;
|
||
}
|
||
}
|
||
|
||
function updateSondeCoefficient(id, coefficient) {
|
||
console.log(`Updating sonde ${id} coefficient to: ${coefficient}`);
|
||
const toastLiveExample = document.getElementById('liveToast');
|
||
const toastBody = toastLiveExample.querySelector('.toast-body');
|
||
|
||
$.ajax({
|
||
url: `launcher.php?type=update_sonde&id=${id}&field=coefficient&value=${coefficient}`,
|
||
dataType: 'json',
|
||
method: 'GET',
|
||
cache: false,
|
||
success: function(response) {
|
||
console.log('Sonde coefficient updated:', response);
|
||
|
||
// Format the response for toast
|
||
let formattedMessage = '';
|
||
|
||
if (response.success) {
|
||
// Success message
|
||
toastLiveExample.classList.remove('text-bg-danger');
|
||
toastLiveExample.classList.add('text-bg-success');
|
||
|
||
formattedMessage = `
|
||
<strong>Success!</strong><br>
|
||
Sonde ID: ${response.id}<br>
|
||
Coefficient: ${coefficient}<br>
|
||
${response.message || ''}
|
||
`;
|
||
} 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>
|
||
Sonde ID: ${id}
|
||
`;
|
||
}
|
||
|
||
// Update and show toast
|
||
toastBody.innerHTML = formattedMessage;
|
||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||
toastBootstrap.show();
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error('Failed to update sonde coefficient:', error);
|
||
|
||
// Show error toast
|
||
toastLiveExample.classList.remove('text-bg-success');
|
||
toastLiveExample.classList.add('text-bg-danger');
|
||
toastBody.innerHTML = `
|
||
<strong>Request Failed!</strong><br>
|
||
Error: ${error}<br>
|
||
Sonde ID: ${id}
|
||
`;
|
||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||
toastBootstrap.show();
|
||
}
|
||
});
|
||
}
|
||
|
||
/*
|
||
____ _ __ __ _
|
||
/ ___| ___ _ ____ _(_) ___ ___| \/ | __ _ _ __ __ _ __ _ ___ _ __ ___ ___ _ __ | |_
|
||
\___ \ / _ \ '__\ \ / / |/ __/ _ \ |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '_ ` _ \ / _ \ '_ \| __|
|
||
___) | __/ | \ 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();
|
||
}
|
||
});
|
||
}
|
||
|
||
/*
|
||
_____ ____ _ _ _
|
||
| ____|_ ____ _____ __ _ | _ \ ___| |_ ___ ___| |_(_) ___ _ __
|
||
| _| | '_ \ \ / / _ \/ _` | | | | |/ _ \ __/ _ \/ __| __| |/ _ \| '_ \
|
||
| |___| | | \ V / __/ (_| | | |_| | __/ || __/ (__| |_| | (_) | | | |
|
||
|_____|_| |_|\_/ \___|\__,_| |____/ \___|\__\___|\___|\__|_|\___/|_| |_|
|
||
|
||
*/
|
||
|
||
function detectEnveaSondes() {
|
||
console.log("Opening Envea detection modal");
|
||
const modal = new bootstrap.Modal(document.getElementById('enveaDetectionModal'));
|
||
modal.show();
|
||
|
||
// Reset modal content
|
||
document.getElementById('detectionProgress').style.display = 'none';
|
||
document.getElementById('detectionResults').innerHTML = '<p>Click "Start Detection" to scan for connected Envea devices.</p>';
|
||
document.getElementById('startDetectionBtn').style.display = 'inline-block';
|
||
}
|
||
|
||
function startEnveaDetection() {
|
||
console.log("Starting Envea device detection");
|
||
|
||
// Show progress spinner
|
||
document.getElementById('detectionProgress').style.display = 'block';
|
||
document.getElementById('detectionResults').innerHTML = '';
|
||
document.getElementById('startDetectionBtn').style.display = 'none';
|
||
|
||
// Test the three ports: ttyAMA3, ttyAMA4, ttyAMA5
|
||
const ports = ['ttyAMA3', 'ttyAMA4', 'ttyAMA5'];
|
||
let completedTests = 0;
|
||
let results = [];
|
||
|
||
ports.forEach(function(port, index) {
|
||
$.ajax({
|
||
url: `launcher.php?type=detect_envea_device&port=${port}`,
|
||
dataType: 'json',
|
||
method: 'GET',
|
||
cache: false,
|
||
timeout: 10000, // 10 second timeout per port
|
||
success: function(response) {
|
||
console.log(`Detection result for ${port}:`, response);
|
||
|
||
results[index] = {
|
||
port: port,
|
||
success: response.success,
|
||
data: response.data || '',
|
||
error: response.error || '',
|
||
detected: response.detected || false,
|
||
device_info: response.device_info || ''
|
||
};
|
||
|
||
completedTests++;
|
||
if (completedTests === ports.length) {
|
||
displayDetectionResults(results);
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error(`Detection failed for ${port}:`, error);
|
||
|
||
results[index] = {
|
||
port: port,
|
||
success: false,
|
||
data: '',
|
||
error: `Request failed: ${error}`,
|
||
detected: false,
|
||
device_info: ''
|
||
};
|
||
|
||
completedTests++;
|
||
if (completedTests === ports.length) {
|
||
displayDetectionResults(results);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function displayDetectionResults(results) {
|
||
console.log("Displaying detection results:", results);
|
||
|
||
// Hide progress spinner
|
||
document.getElementById('detectionProgress').style.display = 'none';
|
||
|
||
let htmlContent = '<h6>Detection Results:</h6>';
|
||
|
||
// Create cards for each port result
|
||
results.forEach(function(result, index) {
|
||
const statusBadge = result.detected ?
|
||
'<span class="badge bg-success">Device Detected</span>' :
|
||
result.success ?
|
||
'<span class="badge bg-warning">No Device</span>' :
|
||
'<span class="badge bg-danger">Error</span>';
|
||
|
||
const deviceInfo = result.device_info || (result.detected ? 'Envea Device' : 'None');
|
||
const rawData = result.data || 'No data';
|
||
|
||
htmlContent += `
|
||
<div class="card mb-3">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h6 class="mb-0"><strong>Port ${result.port}</strong></h6>
|
||
${statusBadge}
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<div class="col-12 mb-3">
|
||
<strong>Device Information:</strong>
|
||
<p class="mb-0">${deviceInfo}</p>
|
||
</div>
|
||
${result.error ? `<div class="col-12 mb-3"><div class="alert alert-danger mb-0"><strong>Error:</strong> ${result.error}</div></div>` : ''}
|
||
<div class="col-12">
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<strong>Raw Data Output:</strong>
|
||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#rawData${index}" aria-expanded="false">
|
||
Toggle Raw Data
|
||
</button>
|
||
</div>
|
||
<div class="collapse" id="rawData${index}">
|
||
<pre class="bg-light p-3 rounded" style="white-space: pre-wrap; word-wrap: break-word; max-height: 300px; overflow-y: auto; font-size: 0.85rem;">${rawData}</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// Add summary
|
||
const detectedCount = results.filter(r => r.detected).length;
|
||
htmlContent += `<div class="alert alert-info mt-3">
|
||
<i class="bi bi-info-circle"></i> <strong>Summary:</strong> ${detectedCount} device(s) detected out of ${results.length} ports tested.
|
||
</div>`;
|
||
|
||
document.getElementById('detectionResults').innerHTML = htmlContent;
|
||
document.getElementById('startDetectionBtn').style.display = 'inline-block';
|
||
document.getElementById('startDetectionBtn').textContent = 'Scan Again';
|
||
}
|
||
|
||
/*
|
||
____ _ _ _ ____ _ _ _
|
||
| _ \ _ __ ___ | |_ ___ ___| |_ ___ __| | / ___| ___| |_| |_(_)_ __ __ _ ___
|
||
| |_) | '__/ _ \| __/ _ \/ __| __/ _ \/ _` | \___ \ / _ \ __| __| | '_ \ / _` / __|
|
||
| __/| | | (_) | || __/ (__| || __/ (_| | ___) | __/ |_| |_| | | | | (_| \__ \
|
||
|_| |_| \___/ \__\___|\___|\__\___|\__,_| |____/ \___|\__|\__|_|_| |_|\__, |___/
|
||
|___/
|
||
*/
|
||
|
||
// Track if protected settings are unlocked
|
||
let protectedSettingsUnlocked = false;
|
||
|
||
function toggleProtectedSettings() {
|
||
const unlockBtn = document.getElementById('unlockBtn');
|
||
const protectedCheckboxes = document.querySelectorAll('.protected-checkbox');
|
||
|
||
if (protectedSettingsUnlocked) {
|
||
// Lock the settings
|
||
protectedSettingsUnlocked = false;
|
||
protectedCheckboxes.forEach(checkbox => {
|
||
checkbox.disabled = true;
|
||
});
|
||
|
||
// Update button appearance
|
||
unlockBtn.classList.remove('btn-success');
|
||
unlockBtn.classList.add('btn-outline-primary');
|
||
unlockBtn.innerHTML = `
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock-fill" viewBox="0 0 16 16">
|
||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||
</svg>
|
||
Unlock
|
||
`;
|
||
|
||
// Show toast notification
|
||
showToast('Protected settings locked', 'info');
|
||
} else {
|
||
// Prompt for password
|
||
const password = prompt('Enter admin password to unlock protected settings:');
|
||
|
||
if (password === '123plouf') {
|
||
// Correct password - unlock the settings
|
||
protectedSettingsUnlocked = true;
|
||
protectedCheckboxes.forEach(checkbox => {
|
||
checkbox.disabled = false;
|
||
});
|
||
|
||
// Update button appearance
|
||
unlockBtn.classList.remove('btn-outline-primary');
|
||
unlockBtn.classList.add('btn-success');
|
||
unlockBtn.innerHTML = `
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-unlock-fill" viewBox="0 0 16 16">
|
||
<path d="M11 1a2 2 0 0 0-2 2v4a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h5V3a3 3 0 0 1 6 0v4a.5.5 0 0 1-1 0V3a2 2 0 0 0-2-2z"/>
|
||
</svg>
|
||
Lock
|
||
`;
|
||
|
||
// Show success toast
|
||
showToast('Protected settings unlocked! You can now edit the checkboxes.', 'success');
|
||
} else if (password !== null) {
|
||
// Wrong password (null means user cancelled)
|
||
showToast('Incorrect password!', 'error');
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
__ __ _ _
|
||
\ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _
|
||
\ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` |
|
||
\ V / __/ | \__ \ | (_) | | | | | | | | (_| |
|
||
\_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, |
|
||
|___/
|
||
*/
|
||
|
||
function loadFirmwareVersion() {
|
||
$.ajax({
|
||
url: 'launcher.php?type=get_firmware_version',
|
||
dataType: 'json',
|
||
method: 'GET',
|
||
cache: false,
|
||
success: function(response) {
|
||
const badge = document.getElementById('firmwareVersionBadge');
|
||
if (response.success) {
|
||
badge.textContent = 'v' + response.version;
|
||
badge.className = 'badge bg-primary';
|
||
} else {
|
||
badge.textContent = 'Version unknown';
|
||
badge.className = 'badge bg-secondary';
|
||
}
|
||
},
|
||
error: function() {
|
||
const badge = document.getElementById('firmwareVersionBadge');
|
||
badge.textContent = 'Version unknown';
|
||
badge.className = 'badge bg-secondary';
|
||
}
|
||
});
|
||
}
|
||
|
||
function showChangelogModal() {
|
||
const modal = new bootstrap.Modal(document.getElementById('changelogModal'));
|
||
modal.show();
|
||
|
||
// Load changelog data
|
||
$.ajax({
|
||
url: 'launcher.php?type=get_changelog',
|
||
dataType: 'json',
|
||
method: 'GET',
|
||
cache: false,
|
||
success: function(response) {
|
||
if (response.success && response.changelog) {
|
||
displayChangelog(response.changelog);
|
||
} else {
|
||
document.getElementById('changelogModalBody').innerHTML =
|
||
'<div class="alert alert-warning">Could not load changelog.</div>';
|
||
}
|
||
},
|
||
error: function() {
|
||
document.getElementById('changelogModalBody').innerHTML =
|
||
'<div class="alert alert-danger">Failed to load changelog.</div>';
|
||
}
|
||
});
|
||
}
|
||
|
||
function displayChangelog(data) {
|
||
const container = document.getElementById('changelogModalBody');
|
||
let html = '';
|
||
|
||
data.versions.forEach(function(version) {
|
||
html += `<div class="card mb-3">`;
|
||
html += `<div class="card-header d-flex justify-content-between align-items-center">`;
|
||
html += `<h5 class="mb-0">v${version.version}</h5>`;
|
||
html += `<span class="text-muted">${version.date}</span>`;
|
||
html += `</div>`;
|
||
html += `<div class="card-body">`;
|
||
|
||
// Features
|
||
if (version.changes.features && version.changes.features.length > 0) {
|
||
html += `<h6 class="text-success">Features</h6><ul>`;
|
||
version.changes.features.forEach(function(f) {
|
||
html += `<li>${f}</li>`;
|
||
});
|
||
html += `</ul>`;
|
||
}
|
||
|
||
// Improvements
|
||
if (version.changes.improvements && version.changes.improvements.length > 0) {
|
||
html += `<h6 class="text-info">Improvements</h6><ul>`;
|
||
version.changes.improvements.forEach(function(i) {
|
||
html += `<li>${i}</li>`;
|
||
});
|
||
html += `</ul>`;
|
||
}
|
||
|
||
// Fixes
|
||
if (version.changes.fixes && version.changes.fixes.length > 0) {
|
||
html += `<h6 class="text-danger">Fixes</h6><ul>`;
|
||
version.changes.fixes.forEach(function(f) {
|
||
html += `<li>${f}</li>`;
|
||
});
|
||
html += `</ul>`;
|
||
}
|
||
|
||
// Compatibility
|
||
if (version.changes.compatibility && version.changes.compatibility.length > 0) {
|
||
html += `<div class="alert alert-warning mt-2 mb-0"><strong>Compatibility:</strong><ul class="mb-0">`;
|
||
version.changes.compatibility.forEach(function(c) {
|
||
html += `<li>${c}</li>`;
|
||
});
|
||
html += `</ul></div>`;
|
||
}
|
||
|
||
// Notes
|
||
if (version.notes) {
|
||
html += `<p class="text-muted mt-2 mb-0"><em>${version.notes}</em></p>`;
|
||
}
|
||
|
||
html += `</div></div>`;
|
||
});
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|