diff --git a/api/contadores_json.php b/api/contadores_json.php
index 343d73c..b7dd970 100644
--- a/api/contadores_json.php
+++ b/api/contadores_json.php
@@ -1,35 +1,35 @@
-query("
- SELECT uid, note, entradas, saidas, online, blocked,
- current_broker, rssi, heap, uptime,
- TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
- FROM contadores
- ORDER BY online DESC, uid ASC
-")->fetchAll();
-
-// Totais
-$sumEntradas = 0;
-$sumSaidas = 0;
-$onlineCount = 0;
-$offlineCount = 0;
-
-foreach ($rows as $r) {
- $sumEntradas += (int)$r['entradas'];
- $sumSaidas += (int)$r['saidas'];
- if ($r['online'] == 1) $onlineCount++;
- else $offlineCount++;
-}
-header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
-header("Pragma: no-cache");
-header("Expires: 0");
-
-echo json_encode([
- "rows" => $rows,
- "online" => $onlineCount,
- "offline" => $offlineCount,
- "entradas" => $sumEntradas,
- "saidas" => $sumSaidas,
-]);
+query("
+ SELECT uid, note, entradas, saidas, online, blocked,
+ current_broker, rssi, heap, uptime,
+ TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
+ FROM contadores
+ ORDER BY online DESC, uid ASC
+")->fetchAll();
+
+// Totais
+$sumEntradas = 0;
+$sumSaidas = 0;
+$onlineCount = 0;
+$offlineCount = 0;
+
+foreach ($rows as $r) {
+ $sumEntradas += (int)$r['entradas'];
+ $sumSaidas += (int)$r['saidas'];
+ if ($r['online'] == 1) $onlineCount++;
+ else $offlineCount++;
+}
+header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
+header("Pragma: no-cache");
+header("Expires: 0");
+
+echo json_encode([
+ "rows" => $rows,
+ "online" => $onlineCount,
+ "offline" => $offlineCount,
+ "entradas" => $sumEntradas,
+ "saidas" => $sumSaidas,
+]);
diff --git a/api/listener_recovery.php b/api/listener_recovery.php
index 763286d..6d8186f 100644
--- a/api/listener_recovery.php
+++ b/api/listener_recovery.php
@@ -50,29 +50,12 @@ $db = new PDO(
]
);
-// =====================================================
-// RECOVERY MQTT
-// =====================================================
-
-$recoveryHost = 'mqtt-recovery.xupas.mywire.org';
-$recoveryPort = 8884;
-
-$recoveryUser = 'recovery';
-$recoveryPass = 'recovery123';
-
-// =====================================================
-// MAIN MQTT
-// =====================================================
-
-$mainHost = 'mqtt.xupas.mywire.org';
-$mainPort = 8883;
-
-// =====================================================
-// CERT
-// =====================================================
-
-$certPath = '/etc/mosquitto/certs/chain.pem'; // cert Let's Encrypt do broker principal
-$certUrl = 'https://mqtt.xupas.mywire.org/cert.pem';
+// ─── migração: coluna auth_nonce_at (idempotente) ──────
+$db->exec("
+ ALTER TABLE contadores
+ ADD COLUMN IF NOT EXISTS
+ auth_nonce_at DATETIME NULL DEFAULT NULL
+");
// =====================================================
// GLOBAL SECRET
@@ -81,28 +64,92 @@ $certUrl = 'https://mqtt.xupas.mywire.org/cert.pem';
$authSecret = 'K82A91F0';
// =====================================================
-// MQTT SETTINGS
+// CA CERT mqtt-recovery.xpto.ooguy.com (self-signed)
// =====================================================
-$settings = (new ConnectionSettings())
- ->setUsername($recoveryUser)
- ->setPassword($recoveryPass)
- ->setUseTls(true)
- ->setTlsSelfSignedAllowed(true)
- ->setKeepAliveInterval(30)
- ->setConnectTimeout(5)
- ->setSocketTimeout(1)
- ->setReconnectAutomatically(true);
+$recoveryXptoCaCert = <<<'EOT'
+-----BEGIN CERTIFICATE-----
+MIIDuTCCAqGgAwIBAgIUass5a34ekbqg0iaygCtiGRiSFHIwDQYJKoZIhvcNAQEL
+BQAwbDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDElMCMGA1UEAwwcbXF0dC1yZWNvdmVy
+eS54cHRvLm9vZ3V5LmNvbTAeFw0yNjA1MjYxMjM1NDFaFw0zNjA1MjMxMjM1NDFa
+MGwxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ
+bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxJTAjBgNVBAMMHG1xdHQtcmVjb3Zlcnku
+eHB0by5vb2d1eS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDe
+2uCWyHP/xng8DlL4SgqlXq9YbZQw/XWIoNDcpCPAirSKW/d6RNoIa4/UoUtOVfUq
+vN7JgAi+Ovz86q+aagYdVgtU/pIsj/U3/uwZ8Ga2g/AWCD2WZawE/SfpIoape6PM
+EnvSjFpyTZCLcOh6xUIdqH2/RcI7WJvgBBFkocZuP12zJD4jAIZC41+LpjUQQ6rM
+DANhmUYD2fVXdQ8f9hWuKH0inal+2eHtY6n4oL3RZdaSQNQz0j5QKeVfMuAKrMNu
+JeUEYku7Exvr6slaeoEVqlj7UwfwCPNernOvleB5y9dMKDNbXeHAFBdrPMQrgAK7
+KZvNq/5caqpzdxZfskJFAgMBAAGjUzBRMB0GA1UdDgQWBBThMl6pM+mkvCeVFwkX
+NTTnE0R2jDAfBgNVHSMEGDAWgBThMl6pM+mkvCeVFwkXNTTnE0R2jDAPBgNVHRMB
+Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDDSbFkl1BIotX6dREzyYh0H4dF
+NTU3zAayZnd4k33oIgpVnpoVd9PBwzRiRc8PuTiQv6iF2vsmJY/xghNp7UlgHo4k
+UEuF15F24fCj9Xqpbm1EgDGcDzwOsS+RD91bIXjt5U4m8S1meoJ9+xxK6LanUwp8
+FSgxiw+NGcaDMH4Dc6U6Qvxo3ycIkaJQ+SuK72RW0naC9oMp+Yq0otUjDpUkkmP9
+fNex9ArIrKPhWqWBRnImVStfY/1nzg9Eh7bS6k+qpVIEfvKVLdt2WfWDDtazaJpq
+eJSAR5CXLy3G8OKZpcSe0YXb2NkDX/R88B7z9uGD6Zz/lHE9Zot+9JS6tcvZ
+-----END CERTIFICATE-----
+EOT;
+
+$recoveryXptoCaFile = '/etc/mosquitto/certs/recovery-xpto-ca.pem';
+
+if (
+ !file_exists($recoveryXptoCaFile) ||
+ trim((string) file_get_contents($recoveryXptoCaFile)) !== trim($recoveryXptoCaCert)
+) {
+ file_put_contents($recoveryXptoCaFile, $recoveryXptoCaCert);
+ logmsg("CA cert escrito: $recoveryXptoCaFile");
+}
// =====================================================
-// MQTT CLIENT
+// BROKER PAIRS
+// =====================================================
+//
+// Cada par agrupa:
+// recvHost/Port/User/Pass → recovery broker (onde o ESP se liga quando o principal cai)
+// recvCaFile → CA para verificar o cert TLS do recovery broker
+// null = usar CA do sistema (Let's Encrypt)
+// selfSigned → true = aceitar certs self-signed (TlsSelfSignedAllowed)
+// mainHost/Port → main broker (destino do SWITCH_PRIMARY)
+// certPath → cert do main broker guardado localmente (enviado ao ESP)
+// certUrl → URL para refrescar o cert do main broker
+//
// =====================================================
-$mqtt = new MqttClient(
- $recoveryHost,
- $recoveryPort,
- 'listener-recovery-' . getmypid()
-);
+$brokerPairs = [
+
+ // ── Remoto — Let's Encrypt ─────────────────────
+ [
+ 'label' => 'remote',
+ 'recvHost' => 'mqtt-recovery.xupas.mywire.org',
+ 'recvPort' => 8884,
+ 'recvUser' => 'recovery',
+ 'recvPass' => 'recovery123',
+ 'recvCaFile' => null, // Let's Encrypt — CA do sistema
+ 'selfSigned' => true, // mantém comportamento original
+ 'mainHost' => 'mqtt.xupas.mywire.org',
+ 'mainPort' => 8883,
+ 'certPath' => '/etc/mosquitto/certs/device-ca.pem',
+ 'certUrl' => 'https://mqtt.xupas.mywire.org/cert.pem',
+ ],
+
+ // ── Local — self-signed ────────────────────────
+ [
+ 'label' => 'local-selfsigned',
+ 'recvHost' => 'mqtt-recovery.xpto.ooguy.com',
+ 'recvPort' => 8884,
+ 'recvUser' => 'xupa',
+ 'recvPass' => 'xupa',
+ 'recvCaFile' => $recoveryXptoCaFile,
+ 'selfSigned' => true,
+ 'mainHost' => 'mqtt.xpto.ooguy.com',
+ 'mainPort' => 8883,
+ 'certPath' => '/etc/mosquitto/certs/device-ca.pem',
+ 'certUrl' => 'https://mqtt.xpto.ooguy.com/cert.pem',
+ ],
+
+];
// =====================================================
// MAIN BROKER CHECK
@@ -206,18 +253,41 @@ function send_cert_chunks(
}
// =====================================================
-// SUBSCRIPTIONS
+// MAKE SETTINGS
// =====================================================
-$subscribe = function () use (
- $mqtt,
- $db,
- $certPath,
- $certUrl,
- $mainHost,
- $mainPort,
- $authSecret
-) {
+function make_settings(array $pair): ConnectionSettings
+{
+ $s = (new ConnectionSettings())
+ ->setUsername($pair['recvUser'])
+ ->setPassword($pair['recvPass'])
+ ->setUseTls(true)
+ ->setTlsSelfSignedAllowed($pair['selfSigned'])
+ ->setKeepAliveInterval(30)
+ ->setConnectTimeout(5)
+ ->setSocketTimeout(1)
+ ->setReconnectAutomatically(true);
+
+ if ($pair['recvCaFile'] !== null) {
+ $s = $s->setTlsCertificateAuthorityFile($pair['recvCaFile']);
+ }
+
+ return $s;
+}
+
+// =====================================================
+// SUBSCRIBE BROKER
+// =====================================================
+
+function subscribe_broker(
+ MqttClient $mqtt,
+ PDO $db,
+ array $pair,
+ string $authSecret
+): void {
+
+ $label = $pair['label'];
+ $certPath = $pair['certPath'];
// =================================================
// AUTH
@@ -226,7 +296,7 @@ $subscribe = function () use (
$mqtt->subscribe(
'esp/+/auth',
function (string $topic, string $msg)
- use ($db, $mqtt, $authSecret)
+ use ($db, $mqtt, $authSecret, $label)
{
$uid = explode('/', $topic)[1] ?? null;
@@ -235,17 +305,12 @@ $subscribe = function () use (
return;
}
- logmsg("AUTH RX $uid => $msg");
+ logmsg("[$label] AUTH RX $uid => $msg");
$json = json_decode($msg, true);
if (!$json) {
-
- logmsg(
- "JSON AUTH inválido",
- LOG_ERROR
- );
-
+ logmsg("[$label] JSON AUTH inválido", LOG_ERROR);
return;
}
@@ -255,41 +320,62 @@ $subscribe = function () use (
if (($json['cmd'] ?? '') === 'HELLO') {
- $nonce =
- strtoupper(
- bin2hex(random_bytes(16))
- );
-
+ // ─── Idempotência: evitar AUTH_CHALLENGE duplicado ─
+ // Quando o listener reinicia, o broker pode reentrogar
+ // mensagens QoS=1 não ACKadas pela instância anterior.
+ // Se já existe um nonce recente (< 30 s) reutilizamo-lo
+ // em vez de gerar um novo e sobrescrever o DB.
$stmt = $db->prepare("
- INSERT INTO contadores
- (uid, auth_nonce, last_seen, online, blocked)
- VALUES
- (:uid, :nonce, NOW(), 1, 1)
- ON DUPLICATE KEY UPDATE
- auth_nonce=:nonce,
- last_seen=NOW(),
- online=1
+ SELECT auth_nonce, auth_nonce_at
+ FROM contadores
+ WHERE uid=:uid
+ LIMIT 1
");
- /* novos ESPs entram com blocked=1 (pendente aprovação)
- ESPs existentes mantêm o blocked actual */
+ $stmt->execute(['uid' => $uid]);
+ $existing = $stmt->fetch(PDO::FETCH_ASSOC);
- $stmt->execute([
- 'uid' => $uid,
- 'nonce' => $nonce
- ]);
+ $existingNonce = $existing['auth_nonce'] ?? null;
+ $nonceAge = $existing['auth_nonce_at']
+ ? (time() - strtotime($existing['auth_nonce_at']))
+ : PHP_INT_MAX;
+
+ if ($existingNonce && $nonceAge < 30) {
+ // Nonce recente — reutilizar sem tocar no DB
+ $nonce = $existingNonce;
+ logmsg("[$label] AUTH_CHALLENGE reutilizado para $uid (age {$nonceAge}s)");
+ } else {
+ $nonce = strtoupper(bin2hex(random_bytes(16)));
+
+ $stmt = $db->prepare("
+ INSERT INTO contadores
+ (uid, auth_nonce, auth_nonce_at, last_seen, online, blocked)
+ VALUES
+ (:uid, :nonce, NOW(), NOW(), 1, 1)
+ ON DUPLICATE KEY UPDATE
+ auth_nonce=:nonce,
+ auth_nonce_at=NOW(),
+ last_seen=NOW(),
+ online=1
+ ");
+ /* novos ESPs entram com blocked=1 (pendente aprovação)
+ ESPs existentes mantêm o blocked actual */
+
+ $stmt->execute([
+ 'uid' => $uid,
+ 'nonce' => $nonce,
+ ]);
+ }
$mqtt->publish(
"esp/$uid/cmd",
json_encode([
- 'cmd' => 'AUTH_CHALLENGE',
- 'nonce' => $nonce
+ 'cmd' => 'AUTH_CHALLENGE',
+ 'nonce' => $nonce,
]),
1
);
- logmsg(
- "AUTH_CHALLENGE enviado para $uid"
- );
+ logmsg("[$label] AUTH_CHALLENGE enviado para $uid");
return;
}
@@ -306,12 +392,7 @@ $subscribe = function () use (
);
if (!$hash) {
-
- logmsg(
- "AUTH sem hash",
- LOG_ERROR
- );
-
+ logmsg("[$label] AUTH sem hash", LOG_ERROR);
return;
}
@@ -326,29 +407,17 @@ $subscribe = function () use (
'uid' => $uid
]);
- $row =
- $stmt->fetch(PDO::FETCH_ASSOC);
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
-
- logmsg(
- "UID desconhecido: $uid",
- LOG_ERROR
- );
-
+ logmsg("[$label] UID desconhecido: $uid", LOG_ERROR);
return;
}
- $nonce =
- $row['auth_nonce'] ?? '';
+ $nonce = $row['auth_nonce'] ?? '';
if (!$nonce) {
-
- logmsg(
- "Nonce vazio para $uid",
- LOG_ERROR
- );
-
+ logmsg("[$label] Nonce vazio para $uid", LOG_ERROR);
return;
}
@@ -366,7 +435,10 @@ $subscribe = function () use (
// verificar se está aprovado
if ((int)($row['blocked'] ?? 1) === 1) {
- logmsg("AUTH PENDENTE/BLOQUEADO $uid — aguarda aprovação", LOG_ERROR);
+ logmsg(
+ "[$label] AUTH PENDENTE/BLOQUEADO $uid — aguarda aprovação",
+ LOG_ERROR
+ );
$mqtt->publish(
"esp/$uid/cmd",
@@ -377,12 +449,13 @@ $subscribe = function () use (
return;
}
- logmsg("AUTH OK $uid");
+ logmsg("[$label] AUTH OK $uid");
$stmt = $db->prepare("
UPDATE contadores
SET
auth_nonce=NULL,
+ auth_nonce_at=NULL,
auth_ok=1,
online=1,
last_seen=NOW()
@@ -403,18 +476,10 @@ $subscribe = function () use (
} else {
- logmsg(
- "AUTH FAIL $uid",
- LOG_ERROR
- );
-
- $mqtt->publish(
- "esp/$uid/cmd",
- json_encode([
- 'cmd' => 'AUTH_FAIL'
- ]),
- 1
- );
+ // Hash nao bate: quase sempre e cross-talk - o device respondeu
+ // ao nonce do OUTRO listener (VPS). NAO mandar AUTH_FAIL (causava
+ // boot-loop). So o listener cujo nonce bate manda AUTH_OK.
+ logmsg("[$label] AUTH hash mismatch $uid - ignorado (cross-talk)", LOG_ERROR);
}
return;
@@ -433,9 +498,7 @@ $subscribe = function () use (
string $msg
) use (
$db,
- $mqtt,
- $mainHost,
- $mainPort
+ $label
) {
$uid = explode('/', $topic)[1] ?? null;
@@ -474,41 +537,7 @@ $subscribe = function () use (
'uid' => $uid
]);
- logmsg(
- "RECOVERY STATUS $uid => $msg"
- );
-
- static $lastCheck = 0;
-
- if (time() - $lastCheck >= 15) {
-
- $lastCheck = time();
-
- logmsg("CHECK broker");
-
- if (
- main_broker_online(
- $mainHost,
- $mainPort
- )
- ) {
-
- logmsg(
- "MAIN broker online"
- );
-
- send_switch_primary(
- $mqtt,
- $db
- );
-
- } else {
-
- logmsg(
- "MAIN broker offline"
- );
- }
- }
+ logmsg("[$label] RECOVERY STATUS $uid => $msg");
},
0
@@ -523,18 +552,15 @@ $subscribe = function () use (
function (
string $topic,
string $msg
- ) {
+ ) use ($label) {
- $uid =
- explode('/', $topic)[1] ?? null;
+ $uid = explode('/', $topic)[1] ?? null;
if (!$uid) {
return;
}
- logmsg(
- "RECOVERY EVENT $uid => $msg"
- );
+ logmsg("[$label] RECOVERY EVENT $uid => $msg");
},
0
@@ -552,100 +578,165 @@ $subscribe = function () use (
) use (
$mqtt,
$certPath,
- $certUrl
+ $label
) {
- $uid =
- explode('/', $topic)[1] ?? null;
+ $uid = explode('/', $topic)[1] ?? null;
if (!$uid) {
return;
}
- logmsg("CERT_REQUEST de $uid → chunks");
+ logmsg("[$label] CERT_REQUEST de $uid → chunks");
send_cert_chunks($mqtt, $uid, $certPath);
},
1
);
-};
+}
// =====================================================
// MAIN LOOP
// =====================================================
+$clients = []; // label => MqttClient
+$nextCheck = []; // label => timestamp da próxima verificação do main broker
+$lastConnectAttempt = []; // label => timestamp da última tentativa de ligação
+
while (true) {
- try {
+ // ── connect / reconnect ────────────────────────
- logmsg(
- "Ligar recovery $recoveryHost:$recoveryPort"
- );
+ foreach ($brokerPairs as $pair) {
- $mqtt->connect($settings, false);
-
- $subscribe();
-
- logmsg(
- "RECOVERY ligado e subscrito"
- );
-
- $nextMainCheck = time() + 10;
- $loopStartedAt = microtime(true);
-
- while ($mqtt->isConnected()) {
-
- $mqtt->loopOnce($loopStartedAt, true, 100000);
-
- if (time() >= $nextMainCheck) {
-
- $nextMainCheck = time() + 15;
-
- logmsg(
- "LOOP recovery",
- LOG_DEBUG
- );
-
- if (
- main_broker_online(
- $mainHost,
- $mainPort
- )
- ) {
-
- logmsg(
- "MAIN broker online"
- );
-
- logmsg("Mandar switch_primary");
-
- send_switch_primary(
- $mqtt,
- $db
- );
-
- } else {
-
- logmsg(
- "MAIN broker ainda offline",
- LOG_DEBUG
- );
- }
- }
+ $label = $pair['label'];
+ if (
+ isset($clients[$label]) &&
+ $clients[$label]->isConnected()
+ ) {
+ continue;
}
- $mqtt->disconnect();
+ // throttle: não tentar mais do que 1x / 5 s por broker
+ if (time() < ($lastConnectAttempt[$label] ?? 0) + 5) {
+ continue;
+ }
- } catch (Throwable $e) {
+ $lastConnectAttempt[$label] = time();
- logmsg(
- "ERRO recovery: " .
- $e->getMessage(),
- LOG_ERROR
- );
+ try {
- sleep(5);
+ logmsg(
+ "Ligar recovery [{$label}] " .
+ "{$pair['recvHost']}:{$pair['recvPort']}"
+ );
+
+ $clients[$label] = new MqttClient(
+ $pair['recvHost'],
+ $pair['recvPort'],
+ 'listener-recovery-' . $label . '-' . getmypid()
+ );
+
+ $clients[$label]->connect(
+ make_settings($pair),
+ false
+ );
+
+ subscribe_broker(
+ $clients[$label],
+ $db,
+ $pair,
+ $authSecret
+ );
+
+ // primeira verificação do main broker após 10 s
+ $nextCheck[$label] = time() + 10;
+
+ logmsg("[{$label}] ligado e subscrito");
+
+ } catch (Throwable $e) {
+
+ logmsg(
+ "[{$label}] ERRO connect: " . $e->getMessage(),
+ LOG_ERROR
+ );
+
+ unset($clients[$label]);
+ }
}
-}
\ No newline at end of file
+
+ if (empty($clients)) {
+ sleep(5);
+ continue;
+ }
+
+ // ── drive all clients (50 ms cada) ────────────
+
+ $loopStartedAt = microtime(true);
+
+ foreach ($clients as $label => $mqtt) {
+
+ try {
+
+ $mqtt->loopOnce($loopStartedAt, true, 50000);
+
+ } catch (Throwable $e) {
+
+ logmsg(
+ "[{$label}] ERRO loop: " . $e->getMessage(),
+ LOG_ERROR
+ );
+
+ try {
+ $mqtt->disconnect();
+ } catch (Throwable $_) {}
+
+ unset($clients[$label]);
+ }
+ }
+
+ // ── periodic main broker check per pair ────────
+
+ $now = time();
+
+ foreach ($brokerPairs as $pair) {
+
+ $label = $pair['label'];
+
+ if (
+ !isset($clients[$label]) ||
+ !$clients[$label]->isConnected()
+ ) {
+ continue;
+ }
+
+ if ($now < ($nextCheck[$label] ?? 0)) {
+ continue;
+ }
+
+ $nextCheck[$label] = $now + 15;
+
+ logmsg("[{$label}] CHECK main broker", LOG_DEBUG);
+
+ if (
+ main_broker_online(
+ $pair['mainHost'],
+ $pair['mainPort']
+ )
+ ) {
+
+ logmsg("[{$label}] MAIN broker online → SWITCH_PRIMARY");
+
+ send_switch_primary($clients[$label], $db);
+
+ } else {
+
+ logmsg(
+ "[{$label}] MAIN broker ainda offline",
+ LOG_DEBUG
+ );
+ }
+ }
+}
diff --git a/api/menu.php b/api/menu.php
index b5698f0..6e32ab2 100644
--- a/api/menu.php
+++ b/api/menu.php
@@ -1,163 +1,3 @@
&1";
-
- $outputVPN = shell_exec($sshCmd);
- } else {
- $outputVPN = "❌ Comando VPN inválido.";
- }
-}
-?>
-
-
-
-
- Menu Principal
-
-
-
-
-
-
-
-
-
- Bem-vindo, = htmlspecialchars($_SESSION['username']) ?> 😎
-
-
-
-
-
-
-
-
🌍 Controlo da VPN
-
-
-
-
-
-
= htmlspecialchars($outputVPN) ?>
-
-
-
-
-
-
-
-
-
-
-
-
+header("Location: /vpn.php");
+exit;
diff --git a/api/mqtt.php b/api/mqtt.php
index b87edf9..f75f5e1 100644
--- a/api/mqtt.php
+++ b/api/mqtt.php
@@ -1,11 +1,11 @@
- /dev/null 2>&1");
-}
+ /dev/null 2>&1");
+}
diff --git a/api/reset_parciais.php b/api/reset_parciais.php
index 1e213e5..65f9413 100644
--- a/api/reset_parciais.php
+++ b/api/reset_parciais.php
@@ -1,44 +1,44 @@
- PDO::ERRMODE_EXCEPTION,
- ]);
-
- // garantir colunas (ignora se já existirem)
- $db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS entradas_total INT NOT NULL DEFAULT 0");
- $db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS saidas_total INT NOT NULL DEFAULT 0");
-
- $uid = $_POST['uid'] ?? '';
- if ($uid === '') {
- header("Location: /console.php?ok=err");
- exit;
- }
-
- $stmt = $db->prepare("
- UPDATE contadores
- SET entradas_total = COALESCE(entradas_total,0) + COALESCE(entradas,0),
- saidas_total = COALESCE(saidas_total,0) + COALESCE(saidas,0),
- entradas = 0,
- saidas = 0
- WHERE uid = :uid
- ");
- $stmt->execute(['uid' => $uid]);
-
- header("Location: /console.php?ok=line");
- exit;
-
-} catch (Throwable $e) {
- header("Location: /console.php?ok=err");
- exit;
-}
+ PDO::ERRMODE_EXCEPTION,
+ ]);
+
+ // garantir colunas (ignora se já existirem)
+ $db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS entradas_total INT NOT NULL DEFAULT 0");
+ $db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS saidas_total INT NOT NULL DEFAULT 0");
+
+ $uid = $_POST['uid'] ?? '';
+ if ($uid === '') {
+ header("Location: /console.php?ok=err");
+ exit;
+ }
+
+ $stmt = $db->prepare("
+ UPDATE contadores
+ SET entradas_total = COALESCE(entradas_total,0) + COALESCE(entradas,0),
+ saidas_total = COALESCE(saidas_total,0) + COALESCE(saidas,0),
+ entradas = 0,
+ saidas = 0
+ WHERE uid = :uid
+ ");
+ $stmt->execute(['uid' => $uid]);
+
+ header("Location: /console.php?ok=line");
+ exit;
+
+} catch (Throwable $e) {
+ header("Location: /console.php?ok=err");
+ exit;
+}
diff --git a/api/reset_pass.php b/api/reset_pass.php
index 0f79f08..1a4c1eb 100644
--- a/api/reset_pass.php
+++ b/api/reset_pass.php
@@ -1,11 +1,11 @@
- PDO::ERRMODE_EXCEPTION
-]);
-
-$newPassword = password_hash("admin123", PASSWORD_DEFAULT);
-
-$stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE username = 'admin'");
-$stmt->execute([$newPassword]);
-
-echo "✅ Password de admin alterada para admin123";
+ PDO::ERRMODE_EXCEPTION
+]);
+
+$newPassword = password_hash("admin123", PASSWORD_DEFAULT);
+
+$stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE username = 'admin'");
+$stmt->execute([$newPassword]);
+
+echo "✅ Password de admin alterada para admin123";
diff --git a/api/send_cmd.php b/api/send_cmd.php
index c6a6dcc..b0ae6cf 100644
--- a/api/send_cmd.php
+++ b/api/send_cmd.php
@@ -1,34 +1,34 @@
-'invalid_request']);
- exit;
-}
-
-$uid = $data['uid'];
-$cmd = $data['cmd'];
-$value = $data['value'] ?? null;
-
-// ----------------------- MQTT -----------------------
-$topic = "esp/$uid/cmd";
-
-$msg = ['cmd' => $cmd];
-if ($value !== null) $msg['value'] = $value;
-
-mqtt_publish($topic, json_encode($msg, JSON_UNESCAPED_SLASHES));
-
-// ----------------------- BD -------------------------
-if ($cmd === 'BLOCK') {
- $stmt = $pdo->prepare("UPDATE contadores SET blocked = ? WHERE uid = ?");
- $stmt->execute([intval($value), $uid]);
-}
-
-echo json_encode(['ok'=>1]);
+'invalid_request']);
+ exit;
+}
+
+$uid = $data['uid'];
+$cmd = $data['cmd'];
+$value = $data['value'] ?? null;
+
+// ----------------------- MQTT -----------------------
+$topic = "esp/$uid/cmd";
+
+$msg = ['cmd' => $cmd];
+if ($value !== null) $msg['value'] = $value;
+
+mqtt_publish($topic, json_encode($msg, JSON_UNESCAPED_SLASHES));
+
+// ----------------------- BD -------------------------
+if ($cmd === 'BLOCK') {
+ $stmt = $pdo->prepare("UPDATE contadores SET blocked = ? WHERE uid = ?");
+ $stmt->execute([intval($value), $uid]);
+}
+
+echo json_encode(['ok'=>1]);
diff --git a/api/sniff_once.php b/api/sniff_once.php
index 466c068..7220989 100644
--- a/api/sniff_once.php
+++ b/api/sniff_once.php
@@ -1,23 +1,23 @@
-setUsername('xupa')->setPassword('xupa');
-$mqtt->connect($settings, true);
-
-$mqtt->subscribe('esp/+/event', function($topic, $message) use (&$data, $mqtt) {
- $data[] = ['topic'=>$topic, 'message'=>$message];
- if (count($data) >= 5) {
- $mqtt->interrupt(); // para o loop depois de 5 msg
- }
-}, 0);
-
-$mqtt->loop(true);
-
-header('Content-Type: application/json');
-echo json_encode($data, JSON_PRETTY_PRINT);
+setUsername('xupa')->setPassword('xupa');
+$mqtt->connect($settings, true);
+
+$mqtt->subscribe('esp/+/event', function($topic, $message) use (&$data, $mqtt) {
+ $data[] = ['topic'=>$topic, 'message'=>$message];
+ if (count($data) >= 5) {
+ $mqtt->interrupt(); // para o loop depois de 5 msg
+ }
+}, 0);
+
+$mqtt->loop(true);
+
+header('Content-Type: application/json');
+echo json_encode($data, JSON_PRETTY_PRINT);
diff --git a/api/validar.php b/api/validar.php
index 854ddfa..54b4d15 100644
--- a/api/validar.php
+++ b/api/validar.php
@@ -33,7 +33,7 @@ try {
$_SESSION['username'] = $user['username'];
// Redireciona para o menu principal
- header("Location: /api/menu.php");
+ header("Location: /painel.php");
exit;
} catch (Exception $e) {
diff --git a/api/vendor/php-mqtt/client/.github/release.yml b/api/vendor/php-mqtt/client/.github/release.yml
index 374affd..2cd9ffd 100644
--- a/api/vendor/php-mqtt/client/.github/release.yml
+++ b/api/vendor/php-mqtt/client/.github/release.yml
@@ -1,25 +1,25 @@
-changelog:
- exclude:
- labels:
- - ignore-for-release
- authors:
- - octocat
- categories:
- - title: Added
- labels:
- - enhancement
- - title: Deprecated
- labels:
- - deprecated
- - title: Removed
- labels:
- - removed
- - title: Fixed
- labels:
- - bug
- - title: Security
- labels:
- - security
- - title: Changed
- labels:
- - "*"
+changelog:
+ exclude:
+ labels:
+ - ignore-for-release
+ authors:
+ - octocat
+ categories:
+ - title: Added
+ labels:
+ - enhancement
+ - title: Deprecated
+ labels:
+ - deprecated
+ - title: Removed
+ labels:
+ - removed
+ - title: Fixed
+ labels:
+ - bug
+ - title: Security
+ labels:
+ - security
+ - title: Changed
+ labels:
+ - "*"
diff --git a/comandosbck.php b/comandosbck.php
index 6ee202e..8f6232a 100644
--- a/comandosbck.php
+++ b/comandosbck.php
@@ -1,55 +1,55 @@
-setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
-} catch (PDOException $e) {
- die("Erro DB: " . $e->getMessage());
-}
-
-$stmt = $db->query("SELECT * FROM comandos ORDER BY id DESC LIMIT 50");
-$comandos = $stmt->fetchAll(PDO::FETCH_ASSOC);
-?>
-
-
-
-
- Histórico de Comandos
-
-
-
-Histórico de Comandos
-
-
- | ID |
- UID |
- CMD |
- Params |
- Status |
- Data |
-
-
-
- | = $cmd['id'] ?> |
- = htmlspecialchars($cmd['uid']) ?> |
- = htmlspecialchars($cmd['cmd']) ?> |
- = htmlspecialchars($cmd['params']) ?> |
- = $cmd['status'] ?> |
- = $cmd['created_at'] ?? '' ?> |
-
-
-
- ⬅️ Voltar
-
-
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+} catch (PDOException $e) {
+ die("Erro DB: " . $e->getMessage());
+}
+
+$stmt = $db->query("SELECT * FROM comandos ORDER BY id DESC LIMIT 50");
+$comandos = $stmt->fetchAll(PDO::FETCH_ASSOC);
+?>
+
+
+
+
+ Histórico de Comandos
+
+
+
+Histórico de Comandos
+
+
+ | ID |
+ UID |
+ CMD |
+ Params |
+ Status |
+ Data |
+
+
+
+ | = $cmd['id'] ?> |
+ = htmlspecialchars($cmd['uid']) ?> |
+ = htmlspecialchars($cmd['cmd']) ?> |
+ = htmlspecialchars($cmd['params']) ?> |
+ = $cmd['status'] ?> |
+ = $cmd['created_at'] ?? '' ?> |
+
+
+
+ ⬅️ Voltar
+
+
diff --git a/config.php b/config.php
index 203fcc2..d879f37 100644
--- a/config.php
+++ b/config.php
@@ -1,11 +1,11 @@
- 'mqtt.xupas.mywire.org',
- 'port' => 8883,
- 'clientId' => 'php-web-' . bin2hex(random_bytes(3)),
- 'username' => 'xupa',
- 'password' => 'xupa',
- 'clean' => true,
- 'keepalive'=> 60,
- 'timeout' => 5,
-];
+ 'mqtt.xupas.mywire.org',
+ 'port' => 8883,
+ 'clientId' => 'php-web-' . bin2hex(random_bytes(3)),
+ 'username' => 'xupa',
+ 'password' => 'xupa',
+ 'clean' => true,
+ 'keepalive'=> 60,
+ 'timeout' => 5,
+];
diff --git a/console.php b/console.php
index 784cac5..99aa049 100644
--- a/console.php
+++ b/console.php
@@ -1,661 +1,661 @@
-prepare("
- UPDATE contadores
- SET note = ?
- WHERE uid = ?
- ");
-
- $stmt->execute([$note, $uid]);
-
- header("Location: console.php");
- exit;
-}
-
-// =====================================================
-// REMOVER
-// =====================================================
-
-if (isset($_GET['remove'])) {
-
- $uid = $_GET['remove'];
-
- $pdo->prepare("
- DELETE FROM contadores
- WHERE uid = ?
- ")->execute([$uid]);
-
- header("Location: console.php");
- exit;
-}
-
-// =====================================================
-// BLOQUEAR / DESBLOQUEAR
-// =====================================================
-
-if (isset($_GET['toggle'])) {
-
- $uid = $_GET['toggle'];
-
- $pdo->prepare("
- UPDATE contadores
- SET blocked = 1 - blocked
- WHERE uid = ?
- ")->execute([$uid]);
-
- header("Location: console.php");
- exit;
-}
-
-// =====================================================
-// SORT
-// =====================================================
-
-$sort = $_GET['sort'] ?? 'state';
-
-$orderSql = ($sort === 'uid')
- ? "ORDER BY uid ASC"
- : "ORDER BY online DESC, uid ASC";
-
-// =====================================================
-// QUERY
-// =====================================================
-
-$sql = "
- SELECT
- uid,
- note,
- entradas,
- saidas,
- online,
- blocked,
- current_broker,
- rssi,
- heap,
- uptime,
- last_seen,
- TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
- FROM contadores
- $orderSql
-";
-
-$stmt = $pdo->query($sql);
-
-$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-
-// =====================================================
-// TOTAIS
-// =====================================================
-
-$sumEntradas = 0;
-$sumSaidas = 0;
-
-$onlineCount = 0;
-$offlineCount = 0;
-
-foreach ($rows as $r) {
-
- $sumEntradas += (int)$r['entradas'];
- $sumSaidas += (int)$r['saidas'];
-
- if ((int)$r['online'] === 1) {
- $onlineCount++;
- } else {
- $offlineCount++;
- }
-}
-?>
-
-
-
-
-
-
-
-MQTT Swarm Console
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
= $onlineCount ?>
-
- 🟢 ONLINE
-
-
-
-
-
-
-
= $offlineCount ?>
-
- 🔴 OFFLINE
-
-
-
-
-
-
-
= $sumEntradas ?>
-
- 💰 ENTRADAS
-
-
-
-
-
-
-
= $sumSaidas ?>
-
- 💸 SAÍDAS
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- | UID |
- NOTA |
- ENTRADAS |
- SAÍDAS |
- ESTADO |
- BROKER |
- RSSI |
- HEAP |
- UPTIME |
- ÚLTIMO |
- AÇÕES |
-
-
-
-
-
-
-
-
-
-
- BLOQUEADO
- ';
-
- } elseif ($online) {
-
- $state =
- '
- ONLINE
- ';
-
- } else {
-
- $state =
- '
- OFFLINE
- ';
- }
-
- if (($r['current_broker'] ?? '') === 'recovery') {
-
- $broker =
- '
- RECOVERY
- ';
-
- } else {
-
- $broker =
- '
- PRIMARY
- ';
- }
-
- $ago = (int)$r['ago'];
-
- $lastSeen = ($ago < 60)
- ? "{$ago}s"
- : round($ago / 60) . "m";
-
- $pulseClass =
- ($ago <= 2 && $online)
- ? "pulse-cell"
- : "";
-
- ?>
-
-
-
- |
- = htmlspecialchars($r['uid']) ?>
- |
-
-
-
-
-
- |
-
- = (int)$r['entradas'] ?> |
-
- = (int)$r['saidas'] ?> |
-
- = $state ?> |
-
- = $broker ?> |
-
-
-
- = isset($r['rssi'])
- ? $r['rssi'] . " dBm"
- : "-" ?>
-
- |
-
-
-
- = isset($r['heap'])
- ? number_format((int)$r['heap'])
- : "-" ?>
-
- |
-
-
-
-
-
- |
-
- = $lastSeen ?> |
-
-
-
-
-
- |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+prepare("
+ UPDATE contadores
+ SET note = ?
+ WHERE uid = ?
+ ");
+
+ $stmt->execute([$note, $uid]);
+
+ header("Location: console.php");
+ exit;
+}
+
+// =====================================================
+// REMOVER
+// =====================================================
+
+if (isset($_GET['remove'])) {
+
+ $uid = $_GET['remove'];
+
+ $pdo->prepare("
+ DELETE FROM contadores
+ WHERE uid = ?
+ ")->execute([$uid]);
+
+ header("Location: console.php");
+ exit;
+}
+
+// =====================================================
+// BLOQUEAR / DESBLOQUEAR
+// =====================================================
+
+if (isset($_GET['toggle'])) {
+
+ $uid = $_GET['toggle'];
+
+ $pdo->prepare("
+ UPDATE contadores
+ SET blocked = 1 - blocked
+ WHERE uid = ?
+ ")->execute([$uid]);
+
+ header("Location: console.php");
+ exit;
+}
+
+// =====================================================
+// SORT
+// =====================================================
+
+$sort = $_GET['sort'] ?? 'state';
+
+$orderSql = ($sort === 'uid')
+ ? "ORDER BY uid ASC"
+ : "ORDER BY online DESC, uid ASC";
+
+// =====================================================
+// QUERY
+// =====================================================
+
+$sql = "
+ SELECT
+ uid,
+ note,
+ entradas,
+ saidas,
+ online,
+ blocked,
+ current_broker,
+ rssi,
+ heap,
+ uptime,
+ last_seen,
+ TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
+ FROM contadores
+ $orderSql
+";
+
+$stmt = $pdo->query($sql);
+
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// =====================================================
+// TOTAIS
+// =====================================================
+
+$sumEntradas = 0;
+$sumSaidas = 0;
+
+$onlineCount = 0;
+$offlineCount = 0;
+
+foreach ($rows as $r) {
+
+ $sumEntradas += (int)$r['entradas'];
+ $sumSaidas += (int)$r['saidas'];
+
+ if ((int)$r['online'] === 1) {
+ $onlineCount++;
+ } else {
+ $offlineCount++;
+ }
+}
+?>
+
+
+
+
+
+
+
+MQTT Swarm Console
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= $onlineCount ?>
+
+ 🟢 ONLINE
+
+
+
+
+
+
+
= $offlineCount ?>
+
+ 🔴 OFFLINE
+
+
+
+
+
+
+
= $sumEntradas ?>
+
+ 💰 ENTRADAS
+
+
+
+
+
+
+
= $sumSaidas ?>
+
+ 💸 SAÍDAS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | UID |
+ NOTA |
+ ENTRADAS |
+ SAÍDAS |
+ ESTADO |
+ BROKER |
+ RSSI |
+ HEAP |
+ UPTIME |
+ ÚLTIMO |
+ AÇÕES |
+
+
+
+
+
+
+
+
+
+
+ BLOQUEADO
+ ';
+
+ } elseif ($online) {
+
+ $state =
+ '
+ ONLINE
+ ';
+
+ } else {
+
+ $state =
+ '
+ OFFLINE
+ ';
+ }
+
+ if (($r['current_broker'] ?? '') === 'recovery') {
+
+ $broker =
+ '
+ RECOVERY
+ ';
+
+ } else {
+
+ $broker =
+ '
+ PRIMARY
+ ';
+ }
+
+ $ago = (int)$r['ago'];
+
+ $lastSeen = ($ago < 60)
+ ? "{$ago}s"
+ : round($ago / 60) . "m";
+
+ $pulseClass =
+ ($ago <= 2 && $online)
+ ? "pulse-cell"
+ : "";
+
+ ?>
+
+
+
+ |
+ = htmlspecialchars($r['uid']) ?>
+ |
+
+
+
+
+
+ |
+
+ = (int)$r['entradas'] ?> |
+
+ = (int)$r['saidas'] ?> |
+
+ = $state ?> |
+
+ = $broker ?> |
+
+
+
+ = isset($r['rssi'])
+ ? $r['rssi'] . " dBm"
+ : "-" ?>
+
+ |
+
+
+
+ = isset($r['heap'])
+ ? number_format((int)$r['heap'])
+ : "-" ?>
+
+ |
+
+
+
+
+
+ |
+
+ = $lastSeen ?> |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/devices.php b/devices.php
index de2e178..c186ab4 100644
--- a/devices.php
+++ b/devices.php
@@ -122,6 +122,7 @@ $pending = $pdo->query("
$approved = $pdo->query("
SELECT uid, note, last_seen, online, blocked, auth_ok, current_broker,
+ COALESCE(brightness, 128) AS brightness,
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
FROM contadores
WHERE blocked=0 OR auth_ok=1
@@ -208,6 +209,7 @@ body { background:#0b0b0b; color:#e0e0e0; font-family:Arial,sans-serif; }
ESTADO |
BROKER |
ÚLTIMO |
+ BRILHO |
AÇÕES |
@@ -231,12 +233,27 @@ body { background:#0b0b0b; color:#e0e0e0; font-family:Arial,sans-serif; }
? 'RECOVERY'
: 'PRIMARY';
?>
+
| = htmlspecialchars($r['uid']) ?> |
= htmlspecialchars($r['note'] ?? '') ?> |
= $state ?> |
= $broker ?> |
= $lastSeen ?> |
+
+
+ >
+
+ = $bri ?>
+
+
+ |
+
+
|