Files
nebuleair_pro_4g/html/admin.html
PaulVua 6d157cd099 v1.9.16: S88 - dropdown port avec labels PCB (NPM1/2/3)
Le sélecteur affiche 'port NPM1 (/dev/ttyAMA5)' au lieu de juste
'/dev/ttyAMA5', pour matcher le silkscreen de la PCB et éviter
les erreurs de branchement. ttyAMA0 et ttyAMA2 (SARA) sont retirés.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 16:39:28 +02:00

2453 lines
98 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
<button class="btn btn-outline-success mb-3 ms-2 btn_powerTest" onclick="runPowerTest()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lightning-charge me-1" viewBox="0 0 16 16">
<path d="M11.251.068a.5.5 0 0 1 .227.58L9.677 6.5H13a.5.5 0 0 1 .364.843l-8 8.5a.5.5 0 0 1-.842-.49L6.323 9.5H3a.5.5 0 0 1-.364-.843l8-8.5a.5.5 0 0 1 .615-.09z"/>
</svg>
Test Power Supply
</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_s88" onchange="update_config_sqlite('S88', this.checked)">
<label class="form-check-label" for="check_s88">
Send CO2 sensor data (Senseair S88)
</label>
<div class="mt-2 ms-4" style="max-width: 250px;">
<label for="s88_port" class="form-label small mb-1">Port UART du capteur S88</label>
<select class="form-select form-select-sm" id="s88_port" onchange="update_config_sqlite('S88_port', this.value)">
<option value="/dev/ttyAMA5">port NPM1 (/dev/ttyAMA5)</option>
<option value="/dev/ttyAMA4">port NPM2 (/dev/ttyAMA4)</option>
<option value="/dev/ttyAMA3">port NPM3 (/dev/ttyAMA3)</option>
</select>
</div>
</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="RTC_utc_time" class="form-label fw-bold fs-5">RTC time (UTC)</label>
<input type="text" class="form-control form-control-lg border-primary" id="RTC_utc_time" disabled>
<small class="text-muted">Module DS3231 avec pile de sauvegarde. Garde l'heure meme hors tension. Horloge de reference du capteur.</small>
</div>
<div class="mb-3">
<label for="browser_utc_time" class="form-label">Browser time (UTC)</label>
<input type="text" class="form-control" id="browser_utc_time" disabled>
<small class="text-muted">Heure de votre appareil (PC/Mac/tablette). Reference pour verifier le RTC.</small>
</div>
<hr>
<details class="mb-3">
<summary class="text-muted" style="cursor:pointer;">System time (non utilise par le capteur)</summary>
<div class="mt-2">
<div class="mb-3">
<label for="sys_local_time" class="form-label">System time (local)</label>
<input type="text" class="form-control form-control-sm" 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 form-control-sm" id="sys_UTC_time" disabled>
</div>
<small class="text-muted">Horloge Linux du Raspberry Pi. Se synchronise via internet (NTP). Non utilisee par le capteur.</small>
</div>
</details>
<div id="alert_container"></div>
<h5 class="mt-4">Synchroniser le RTC</h5>
<small class="text-muted d-block mb-2">Met a jour l'horloge RTC pour qu'elle reste precise sans internet.</small>
<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">
1. Telecharger le .zip depuis <a href="http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g/releases" target="_blank">Gitea (releases)</a>
ou <a href="http://gitea.aircarto.fr/PaulVua/nebuleair_pro_4g/archive/main.zip" target="_blank">derniere version (main.zip)</a><br>
2. Deposer le fichier .zip ci-dessus puis cliquer sur Upload & Install
</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">Mise à jour en cours</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">
<!-- Live progress UI -->
<div id="updateProgressSection">
<div class="d-flex justify-content-between align-items-center mb-1">
<strong id="updateStepLabel" class="text-primary">Démarrage...</strong>
<span class="text-muted small">
<span id="updateTimerElapsed">00:00</span> / <span id="updateTimerEstimate" class="text-muted">~01:30</span>
</span>
</div>
<div class="progress mb-2" style="height: 22px;">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary"
id="updateProgressBar" role="progressbar"
style="width: 0%; transition: width 0.5s ease;">0%</div>
</div>
<small class="text-muted d-block mb-3">
<i class="bi bi-info-circle"></i> Ne pas fermer ni rafraîchir cette page pendant la mise à jour.
</small>
</div>
<!-- Final status banner (hidden until done) -->
<div id="updateFinalStatus" class="alert mb-3" style="display: none;"></div>
<!-- Collapsible technical log -->
<details id="updateTechLogDetails">
<summary class="text-muted small mb-2" style="cursor: pointer;">
Logs techniques (cliquer pour ouvrir)
</summary>
<pre id="updateOutputContent" class="mb-0 mt-2" style="max-height: 400px; overflow-y: auto; font-size: 0.85rem; background-color: #f8f9fa; padding: 1rem; border-radius: 0.375rem;"></pre>
</details>
</div>
</div>
</div>
</div>
</div>
<!-- TAILSCALE SECTION -->
<div class="row mb-3">
<div class="col-lg-8 col-12">
<h4 class="mt-4">Réseau Tailscale</h4>
<div id="tailscale-card" class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="fw-bold">État de la connexion au tailnet AirCarto</span>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="refreshTailscaleInfo()">
<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>
Actualiser
</button>
</div>
<div class="card-body">
<dl class="row mb-2">
<dt class="col-sm-3 text-muted">Statut</dt>
<dd class="col-sm-9"><span id="tailscaleStatus" class="badge bg-secondary">Chargement…</span></dd>
<dt class="col-sm-3 text-muted">IP tailnet</dt>
<dd class="col-sm-9"><code id="tailscaleIp"></code></dd>
<dt class="col-sm-3 text-muted">Hostname</dt>
<dd class="col-sm-9"><code id="tailscaleHostname"></code></dd>
<dt class="col-sm-3 text-muted">Serveur</dt>
<dd class="col-sm-9"><code id="tailscaleLoginServer"></code></dd>
</dl>
<div id="tailscaleMessage" class="alert alert-warning small mb-2" style="display: none;"></div>
<details>
<summary class="text-muted small" style="cursor: pointer;">Logs bootstrap (cliquer pour ouvrir)</summary>
<pre id="tailscaleLog" class="mb-0 mt-2" style="max-height: 250px; overflow-y: auto; font-size: 0.8rem; background-color: #f8f9fa; padding: 0.75rem; border-radius: 0.375rem;">Chargement…</pre>
</details>
</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);
window._adminConfig = 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_s88 = document.getElementById("check_s88");
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_s88.checked = response["S88"];
if (response["S88_port"]) {
document.getElementById("s88_port").value = response["S88_port"];
}
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;
// Display browser time in UTC
const browserDate = new Date();
const browserUTC = browserDate.getUTCFullYear() + '-' +
String(browserDate.getUTCMonth() + 1).padStart(2, '0') + '-' +
String(browserDate.getUTCDate()).padStart(2, '0') + ' ' +
String(browserDate.getUTCHours()).padStart(2, '0') + ':' +
String(browserDate.getUTCMinutes()).padStart(2, '0') + ':' +
String(browserDate.getUTCSeconds()).padStart(2, '0');
document.getElementById("browser_utc_time").value = browserUTC;
// Compare RTC time with browser time
const alertContainer = document.getElementById("alert_container");
alertContainer.innerHTML = "";
const rtcInput = document.getElementById("RTC_utc_time");
if (response.rtc_module_time === 'not connected' || !response.rtc_module_time) {
// RTC module disconnected
rtcInput.classList.add('border-danger', 'text-danger');
rtcInput.classList.remove('border-primary');
alertContainer.innerHTML = `
<div class="alert alert-danger d-flex align-items-center" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-exclamation-triangle-fill me-2 flex-shrink-0" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.436-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
<div>
<strong>Module RTC deconnecte !</strong><br>
Verifiez la pile du module DS3231 et les cables I2C.
</div>
</div>`;
} else {
const rtcDate = new Date(response.rtc_module_time + ' UTC');
const timeDiff = Math.abs(Math.round((browserDate - rtcDate) / 1000));
if (timeDiff <= 30) {
alertContainer.innerHTML = `
<div class="alert alert-success" role="alert">
RTC synchronise avec l'heure du navigateur (ecart: ${timeDiff} sec).
</div>`;
} else {
const minutes = Math.floor(timeDiff / 60);
const label = minutes > 0 ? `${minutes} min ${timeDiff % 60} sec` : `${timeDiff} sec`;
alertContainer.innerHTML = `
<div class="alert alert-danger" role="alert">
RTC desynchronise ! Ecart avec le navigateur: ${label}.
Utilisez "Synchroniser le RTC" ci-dessous.
</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();
// Load Tailscale connection info
refreshTailscaleInfo();
} //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&param='+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&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);
}
});
}
// Mapping of step markers detected in the log -> progress %, step label, and weight
// for sub-step interpolation. The online script normally takes ~80-100s end-to-end.
const UPDATE_STEPS_ONLINE = [
{ marker: 'Step 1:', percent: 5, label: 'Téléchargement du firmware' },
{ marker: 'Step 2:', percent: 12, label: 'Mise à jour de la configuration BDD' },
{ marker: 'Step 3:', percent: 18, label: 'Vérification des permissions' },
{ marker: 'Step 3c:', percent: 25, label: 'Reconfiguration des services systemd' },
{ marker: 'Step 4:', percent: 70, label: 'Redémarrage des services' },
{ marker: 'Step 5:', percent: 94, label: 'Vérification système' },
{ marker: 'Step 6:', percent: 98, label: 'Nettoyage des logs' },
{ marker: 'completed successfully!', percent: 100, label: '✅ Terminé !' }
];
// Offline upload uses a different shell script with different step numbering.
const UPDATE_STEPS_OFFLINE = [
{ marker: 'Step 1:', percent: 5, label: 'Validation du package' },
{ marker: 'Step 2:', percent: 12, label: 'Synchronisation des fichiers' },
{ marker: 'Step 3:', percent: 20, label: 'Mise à jour de la configuration BDD' },
{ marker: 'Step 4:', percent: 25, label: 'Vérification des permissions' },
{ marker: 'Step 4c:', percent: 30, label: 'Reconfiguration des services systemd' },
{ marker: 'Step 5:', percent: 75, label: 'Redémarrage des services' },
{ marker: 'Step 6:', percent: 94, label: 'Vérification système' },
{ marker: 'Step 7:', percent: 98, label: 'Nettoyage des fichiers temporaires' },
{ marker: 'completed successfully!', percent: 100, label: '✅ Terminé !' }
];
let UPDATE_STEPS = UPDATE_STEPS_ONLINE;
let updatePollState = null;
function updateFirmware() {
// Check if connected to internet (not in hotspot mode)
if (window._adminConfig && window._adminConfig.WIFI_status === 'hotspot') {
alert('Mise à jour impossible en mode hotspot.\nConnectez d\'abord le capteur à un réseau WiFi avec accès internet.');
return;
}
console.log("Starting comprehensive firmware update (background mode)...");
// UI elements
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');
const finalStatus = document.getElementById('updateFinalStatus');
const reloadBtn = document.getElementById('reloadBtn');
// Reset UI
UPDATE_STEPS = UPDATE_STEPS_ONLINE;
updateBtn.disabled = true;
updateBtnText.textContent = 'Updating...';
updateSpinner.style.display = 'inline-block';
updateOutput.style.display = 'block';
updateOutputContent.textContent = '';
finalStatus.style.display = 'none';
finalStatus.className = 'alert mb-3';
reloadBtn.style.display = 'none';
setProgress(0, 'Démarrage...');
// Initial timer
const startTime = Date.now();
updateTimer(startTime);
const timerInterval = setInterval(() => updateTimer(startTime), 1000);
// Start the background update
$.ajax({
url: 'launcher.php?type=update_firmware_start',
method: 'GET',
dataType: 'json',
timeout: 10000,
success: function(response) {
if (!response.success) {
clearInterval(timerInterval);
if (response.error_type === 'sudoers_missing') {
showSudoersMissingError(response);
} else {
showUpdateError('Impossible de lancer la mise à jour: ' + (response.message || response.error || 'erreur inconnue'));
}
resetUpdateButton();
return;
}
// Begin polling progress
updatePollState = { offset: 0, allContent: '', startTime, timerInterval };
pollUpdateProgress();
},
error: function(xhr, status, error) {
clearInterval(timerInterval);
showUpdateError('Erreur de communication: ' + error);
resetUpdateButton();
}
});
}
function pollUpdateProgress() {
if (!updatePollState) return;
$.ajax({
url: 'launcher.php?type=update_firmware_progress&offset=' + updatePollState.offset,
method: 'GET',
dataType: 'json',
timeout: 5000,
success: function(response) {
if (!response.success) {
finishUpdate(false, 'Réponse serveur invalide');
return;
}
// Append new content
if (response.content) {
updatePollState.allContent += response.content;
updatePollState.offset = response.offset;
appendUpdateLog(response.content);
refreshProgressFromContent(updatePollState.allContent);
}
// Check if finished
if (response.done) {
const success = updatePollState.allContent.includes('Update completed successfully!')
&& !updatePollState.allContent.includes('EXIT_CODE=1');
finishUpdate(success);
return;
}
// Continue polling
setTimeout(pollUpdateProgress, 700);
},
error: function(xhr, status, error) {
// Network blip: retry once after a short delay
setTimeout(pollUpdateProgress, 1500);
}
});
}
function appendUpdateLog(newContent) {
const pre = document.getElementById('updateOutputContent');
const formatted = newContent
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\[\d{2}:\d{2}:\d{2}\]/g, '<span style="color: #007bff; font-weight: bold;">$&</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>');
pre.innerHTML += formatted;
pre.scrollTop = pre.scrollHeight;
}
function refreshProgressFromContent(allContent) {
// Find the last (most advanced) step marker present in the log
let bestStep = null;
for (const step of UPDATE_STEPS) {
if (allContent.includes(step.marker)) bestStep = step;
}
if (!bestStep) return;
let percent = bestStep.percent;
let label = bestStep.label;
// Sub-step interpolation: the longest steps benefit from finer-grained
// progress to make the bar feel alive on slow stages.
// - Online "Step 3c:" / Offline "Step 4c:" both run setup_services.sh
// - Online "Step 4:" / Offline "Step 5:" both restart services
if (bestStep.marker === 'Step 3c:' || bestStep.marker === 'Step 4c:') {
const startedCount = (allContent.match(/Started [\w-]+(?: timer| service)/g) || []).length;
// setup_services.sh starts ~11 services. Online: 25 -> 65. Offline: 30 -> 70.
const base = bestStep.percent;
const span = (bestStep.marker === 'Step 3c:') ? 40 : 40;
percent = Math.min(base + span, base + startedCount * 4);
} else if (
(UPDATE_STEPS === UPDATE_STEPS_ONLINE && bestStep.marker === 'Step 4:') ||
(UPDATE_STEPS === UPDATE_STEPS_OFFLINE && bestStep.marker === 'Step 5:')
) {
const restartedCount = (allContent.match(/Restarting enabled service:/g) || []).length;
// ~7 services restarted -> spans base -> base+21
const base = bestStep.percent;
percent = Math.min(base + 21, base + restartedCount * 3);
}
setProgress(percent, label);
}
function setProgress(percent, label) {
const bar = document.getElementById('updateProgressBar');
bar.style.width = percent + '%';
bar.textContent = percent + '%';
if (label) document.getElementById('updateStepLabel').textContent = label;
}
function updateTimer(startTime) {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
const ss = String(elapsed % 60).padStart(2, '0');
document.getElementById('updateTimerElapsed').textContent = `${mm}:${ss}`;
}
function finishUpdate(success, errorMsg) {
if (updatePollState && updatePollState.timerInterval) {
clearInterval(updatePollState.timerInterval);
}
const finalStatus = document.getElementById('updateFinalStatus');
const bar = document.getElementById('updateProgressBar');
const reloadBtn = document.getElementById('reloadBtn');
const techDetails = document.getElementById('updateTechLogDetails');
if (success) {
setProgress(100, '✅ Terminé !');
bar.classList.remove('progress-bar-animated', 'bg-primary');
bar.classList.add('bg-success');
finalStatus.className = 'alert alert-success mb-3';
finalStatus.innerHTML = '<strong>Mise à jour terminée avec succès.</strong> Vous pouvez recharger la page pour voir la nouvelle version.';
finalStatus.style.display = 'block';
reloadBtn.style.display = 'inline-block';
showToast('Update completed successfully!', 'success');
} else {
bar.classList.remove('progress-bar-animated', 'bg-primary');
bar.classList.add('bg-danger');
finalStatus.className = 'alert alert-danger mb-3';
finalStatus.innerHTML = '<strong>Échec de la mise à jour.</strong> '
+ (errorMsg ? errorMsg + ' ' : '')
+ 'Consultez les logs techniques ci-dessous pour plus de détails.';
finalStatus.style.display = 'block';
// Auto-open the technical logs on failure so the user sees what went wrong
if (techDetails) techDetails.open = true;
showToast('Update failed - check technical logs', 'error');
}
resetUpdateButton();
updatePollState = null;
}
function showUpdateError(message) {
const finalStatus = document.getElementById('updateFinalStatus');
finalStatus.className = 'alert alert-danger mb-3';
finalStatus.textContent = message;
finalStatus.style.display = 'block';
}
// Specific error display for the "sudoers missing" pre-flight failure.
// Shows a clear explanation and the exact SSH commands to apply the fix,
// with a copy-to-clipboard button so the user can paste it on the sensor.
function showSudoersMissingError(response) {
const finalStatus = document.getElementById('updateFinalStatus');
const fixCommand = `sudo nano /etc/sudoers.d/nebuleair
# Then paste the content shown below, save (Ctrl+O, Enter, Ctrl+X), then:
sudo chmod 0440 /etc/sudoers.d/nebuleair
sudo visudo -c`;
const sudoersContent = `# NebuleAir Pro 4G sudo rules
ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot
www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull
www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh
www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 *
www-data ALL=(ALL) NOPASSWD: /bin/systemctl *
www-data ALL=(ALL) NOPASSWD: /usr/bin/pkill *
www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*`;
finalStatus.className = 'alert alert-warning mb-3';
finalStatus.innerHTML = `
<h5 class="alert-heading"><i class="bi bi-exclamation-triangle-fill"></i> Configuration sudoers manquante</h5>
<p class="mb-2">${escapeHtml(response.message || '')}</p>
<p class="mb-1"><strong>Fix (SSH sur le capteur) :</strong></p>
<ol class="mb-2">
<li>Connectez-vous en SSH au capteur</li>
<li>Exécutez <code>sudo nano /etc/sudoers.d/nebuleair</code></li>
<li>Collez le contenu ci-dessous puis sauvez (<kbd>Ctrl+O</kbd>, <kbd>Enter</kbd>, <kbd>Ctrl+X</kbd>)</li>
<li>Exécutez <code>sudo chmod 0440 /etc/sudoers.d/nebuleair && sudo visudo -c</code></li>
<li>Relancez la mise à jour ici</li>
</ol>
<div class="position-relative">
<pre class="bg-light p-2 rounded mb-1" style="font-size: 0.8rem; max-height: 200px; overflow-y: auto;"><code id="sudoersFixContent">${escapeHtml(sudoersContent)}</code></pre>
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="copySudoersFix()">
<i class="bi bi-clipboard"></i> Copier le contenu
</button>
</div>
${response.raw ? '<details class="mt-2"><summary class="small text-muted">Sortie technique sudo</summary><pre class="small mt-1 mb-0">' + escapeHtml(response.raw) + '</pre></details>' : ''}
`;
finalStatus.style.display = 'block';
}
function copySudoersFix() {
const content = document.getElementById('sudoersFixContent').textContent;
navigator.clipboard.writeText(content).then(() => {
showToast('Contenu copié dans le presse-papier', 'success');
}).catch(() => {
showToast('Échec de la copie — sélectionnez et copiez manuellement', 'warning');
});
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function resetUpdateButton() {
// Online update button
const updateBtn = document.getElementById('updateBtn');
const updateBtnText = document.getElementById('updateBtnText');
const updateSpinner = document.getElementById('updateSpinner');
if (updateBtn) updateBtn.disabled = false;
if (updateBtnText) updateBtnText.textContent = 'Update firmware';
if (updateSpinner) updateSpinner.style.display = 'none';
// Offline upload button (in case the run was triggered via uploadFirmware)
const uploadBtn = document.getElementById('uploadBtn');
const uploadBtnText = document.getElementById('uploadBtnText');
const uploadSpinner = document.getElementById('uploadSpinner');
const uploadProgressBar = document.getElementById('uploadProgressBar');
if (uploadBtn) uploadBtn.disabled = false;
if (uploadBtnText) uploadBtnText.textContent = 'Upload & Install';
if (uploadSpinner) uploadSpinner.style.display = 'none';
if (uploadProgressBar) uploadProgressBar.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');
const finalStatus = document.getElementById('updateFinalStatus');
const reloadBtn = document.getElementById('reloadBtn');
// Reset and show UI
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.innerHTML = '';
finalStatus.style.display = 'none';
finalStatus.className = 'alert mb-3';
reloadBtn.style.display = 'none';
setProgress(0, 'Téléversement du fichier...');
// Build FormData
const formData = new FormData();
formData.append('firmware_file', file);
const xhr = new XMLHttpRequest();
xhr.timeout = 300000; // 5 minutes for the upload itself
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...';
}
}
});
xhr.addEventListener('load', function() {
let response;
try {
response = JSON.parse(xhr.responseText);
} catch (e) {
finalStatus.className = 'alert alert-danger mb-3';
finalStatus.textContent = 'Erreur: réponse serveur invalide';
finalStatus.style.display = 'block';
resetUploadUI();
showToast('Upload failed: invalid server response', 'error');
return;
}
if (!response.success) {
if (response.error_type === 'sudoers_missing') {
showSudoersMissingError(response);
} else {
finalStatus.className = 'alert alert-danger mb-3';
finalStatus.textContent = 'Erreur: ' + (response.message || 'Erreur inconnue');
finalStatus.style.display = 'block';
}
resetUploadUI();
showToast('Upload failed: ' + (response.message || 'Unknown error'), 'error');
return;
}
// Upload + extraction OK → script is running in background, switch to polling
progressBar.style.display = 'none'; // hide upload bar, the update progress bar takes over
showToast('Upload OK, installation en cours...', 'info');
// Use offline step mapping
UPDATE_STEPS = UPDATE_STEPS_OFFLINE;
// Initial timer
const startTime = Date.now();
updateTimer(startTime);
const timerInterval = setInterval(() => updateTimer(startTime), 1000);
updatePollState = { offset: 0, allContent: '', startTime, timerInterval };
setProgress(2, 'Démarrage de l\'installation...');
pollUpdateProgress();
});
xhr.addEventListener('error', function() {
finalStatus.className = 'alert alert-danger mb-3';
finalStatus.textContent = 'Erreur réseau pendant l\'upload';
finalStatus.style.display = 'block';
resetUploadUI();
showToast('Upload failed: network error', 'error');
});
xhr.addEventListener('timeout', function() {
finalStatus.className = 'alert alert-danger mb-3';
finalStatus.textContent = 'Upload interrompu (timeout 5 min)';
finalStatus.style.display = 'block';
resetUploadUI();
showToast('Upload timed out', 'error');
});
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');
const finalStatus = document.getElementById('updateFinalStatus');
const bar = document.getElementById('updateProgressBar');
updateOutputContent.innerHTML = '';
updateOutput.style.display = 'none';
reloadBtn.style.display = 'none';
if (finalStatus) finalStatus.style.display = 'none';
if (bar) {
bar.style.width = '0%';
bar.textContent = '0%';
bar.classList.remove('bg-success', 'bg-danger');
bar.classList.add('progress-bar-animated', 'bg-primary');
}
}
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 refreshTailscaleInfo() {
$.ajax({
url: 'launcher.php?type=get_tailscale_info',
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
const statusBadge = document.getElementById('tailscaleStatus');
const ipEl = document.getElementById('tailscaleIp');
const hostEl = document.getElementById('tailscaleHostname');
const serverEl = document.getElementById('tailscaleLoginServer');
const msgEl = document.getElementById('tailscaleMessage');
serverEl.textContent = response.login_server || '—';
if (!response.installed) {
statusBadge.textContent = 'Non installé';
statusBadge.className = 'badge bg-secondary';
ipEl.textContent = '—';
hostEl.textContent = '—';
msgEl.style.display = 'block';
msgEl.textContent = response.message || 'Tailscale non installé.';
} else if (response.connected) {
statusBadge.textContent = '✓ Connecté';
statusBadge.className = 'badge bg-success';
ipEl.textContent = response.ip || '—';
hostEl.textContent = response.hostname || '—';
msgEl.style.display = 'none';
} else {
statusBadge.textContent = '✗ Déconnecté';
statusBadge.className = 'badge bg-danger';
ipEl.textContent = '—';
hostEl.textContent = '—';
msgEl.style.display = 'block';
msgEl.textContent = "Tailscale est installé mais n'est pas connecté au tailnet. Vérifier le log bootstrap ci-dessous ou relancer un Update firmware.";
}
refreshTailscaleLog();
},
error: function() {
const statusBadge = document.getElementById('tailscaleStatus');
statusBadge.textContent = 'Erreur';
statusBadge.className = 'badge bg-warning';
}
});
}
function refreshTailscaleLog() {
$.ajax({
url: 'launcher.php?type=get_tailscale_log',
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
const logEl = document.getElementById('tailscaleLog');
if (response.success && response.log) {
logEl.textContent = response.log;
} else {
logEl.textContent = response.message || '(log vide)';
}
},
error: function() {
document.getElementById('tailscaleLog').textContent = '(erreur de chargement du log)';
}
});
}
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>