diff --git a/VERSION b/VERSION
index f8e233b..9ab8337 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.9.0
+1.9.1
diff --git a/changelog.json b/changelog.json
index 0542337..df4cbbd 100644
--- a/changelog.json
+++ b/changelog.json
@@ -1,5 +1,24 @@
{
"versions": [
+ {
+ "version": "1.9.1",
+ "date": "2026-05-19",
+ "changes": {
+ "features": [
+ "Admin: nouvelle section 'Réseau Tailscale' affichant statut de connexion, IP tailnet, hostname et serveur Headscale",
+ "Admin: bouton 'Actualiser' pour rafraîchir les infos Tailscale, et bloc déroulant pour consulter les 50 dernières lignes du log bootstrap",
+ "launcher.php: nouvelles actions get_tailscale_info et get_tailscale_log"
+ ],
+ "improvements": [
+ "Vérification visuelle immédiate de l'état d'enrôlement Tailscale sans avoir à passer en SSH"
+ ],
+ "fixes": [],
+ "compatibility": [
+ "Aucun impact sur les capteurs sans Tailscale: la section affiche 'Non installé' avec un message d'invite à mettre à jour"
+ ]
+ },
+ "notes": "Complète la v1.9.0 (enrôlement automatique) avec la visibilité UI nécessaire pour valider/diagnostiquer la connexion Tailscale sur chaque capteur depuis l'admin web."
+ },
{
"version": "1.9.0",
"date": "2026-05-19",
diff --git a/html/admin.html b/html/admin.html
index cb24e89..769dc70 100755
--- a/html/admin.html
+++ b/html/admin.html
@@ -339,6 +339,45 @@
+
+
+
+
Réseau Tailscale
+
+
+
+
+ - Statut
+ - Chargement…
+
+ - IP tailnet
+ —
+
+ - Hostname
+ —
+
+ - Serveur
+ —
+
+
+
+ Logs bootstrap (cliquer pour ouvrir)
+ Chargement…
+
+
+
+
+
+
@@ -702,6 +741,9 @@ window.onload = function() {
// Load firmware version
loadFirmwareVersion();
+ // Load Tailscale connection info
+ refreshTailscaleInfo();
+
} //end window.onload
@@ -2225,6 +2267,73 @@ function loadFirmwareVersion() {
});
}
+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();
diff --git a/html/launcher.php b/html/launcher.php
index 094d3c9..e4a8cd2 100755
--- a/html/launcher.php
+++ b/html/launcher.php
@@ -2050,6 +2050,69 @@ if ($type == "get_changelog") {
}
}
+// Get Tailscale connection info (status, IP, hostname, login server)
+// Used by the "Réseau Tailscale" card on admin.html.
+if ($type == "get_tailscale_info") {
+ $login_server = 'https://headscale.aircarto.fr';
+ $tailscale_bin = '/usr/bin/tailscale';
+
+ if (!file_exists($tailscale_bin)) {
+ echo json_encode([
+ 'installed' => false,
+ 'connected' => false,
+ 'ip' => '',
+ 'hostname' => '',
+ 'login_server' => $login_server,
+ 'message' => 'Tailscale non installé sur ce capteur (mettre à jour vers v1.9.0+).'
+ ]);
+ return;
+ }
+
+ // tailscaled socket is root-owned, so we need sudo (NOPASSWD rule added in v1.9.0).
+ $ip = trim(shell_exec("sudo $tailscale_bin ip -4 2>/dev/null") ?? '');
+ $hostname = '';
+ $status_raw = shell_exec("sudo $tailscale_bin status 2>/dev/null") ?? '';
+
+ if ($ip !== '' && $status_raw !== '') {
+ foreach (explode("\n", $status_raw) as $line) {
+ if (strpos($line, $ip) === 0) {
+ $parts = preg_split('/\s+/', trim($line));
+ $hostname = $parts[1] ?? '';
+ break;
+ }
+ }
+ }
+
+ echo json_encode([
+ 'installed' => true,
+ 'connected' => $ip !== '',
+ 'ip' => $ip,
+ 'hostname' => $hostname,
+ 'login_server' => $login_server
+ ]);
+ return;
+}
+
+// Get last lines of the Tailscale bootstrap log
+if ($type == "get_tailscale_log") {
+ $logFile = '/var/www/nebuleair_pro_4g/logs/tailscale_bootstrap.log';
+ if (!file_exists($logFile)) {
+ echo json_encode([
+ 'success' => false,
+ 'log' => '',
+ 'message' => "Pas encore de log (bootstrap jamais exécuté). Le log apparaîtra au prochain update ou reboot."
+ ]);
+ return;
+ }
+ // tail -n 50 equivalent
+ $output = shell_exec("tail -n 50 " . escapeshellarg($logFile) . " 2>/dev/null") ?? '';
+ echo json_encode([
+ 'success' => true,
+ 'log' => $output
+ ]);
+ return;
+}
+
// Get current CPU power mode
if ($type == "get_cpu_power_mode") {
try {