Added distributed MQTT recovery backend and device governance
This commit is contained in:
parent
269ea239f2
commit
a028fc6497
@ -3,7 +3,8 @@ declare(strict_types=1);
|
|||||||
require __DIR__ . "/db.php";
|
require __DIR__ . "/db.php";
|
||||||
|
|
||||||
$rows = $pdo->query("
|
$rows = $pdo->query("
|
||||||
SELECT uid, entradas, saidas, online, blocked,
|
SELECT uid, note, entradas, saidas, online, blocked,
|
||||||
|
current_broker, rssi, heap, uptime,
|
||||||
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
||||||
FROM contadores
|
FROM contadores
|
||||||
ORDER BY online DESC, uid ASC
|
ORDER BY online DESC, uid ASC
|
||||||
|
|||||||
@ -9,17 +9,27 @@ $LOG_LEVEL = LOG_DEBUG;
|
|||||||
//$LOG_LEVEL = LOG_INFO;
|
//$LOG_LEVEL = LOG_INFO;
|
||||||
|
|
||||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
|
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
|
||||||
|
|
||||||
ini_set('display_errors', '0');
|
ini_set('display_errors', '0');
|
||||||
ini_set('log_errors', '1');
|
ini_set('log_errors', '1');
|
||||||
ini_set('error_log', '/var/log/mqtt-listener/recovery-error.log');
|
ini_set('error_log', '/var/log/mqtt-listener/recovery-error.log');
|
||||||
|
|
||||||
set_time_limit(0);
|
set_time_limit(0);
|
||||||
ignore_user_abort(true);
|
ignore_user_abort(true);
|
||||||
|
|
||||||
function logmsg(string $msg, int $level = LOG_INFO): void {
|
function logmsg(string $msg, int $level = LOG_INFO): void
|
||||||
|
{
|
||||||
global $LOG_LEVEL;
|
global $LOG_LEVEL;
|
||||||
if ($level > $LOG_LEVEL) return;
|
|
||||||
|
if ($level > $LOG_LEVEL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
echo '[' . date('Y-m-d H:i:s') . "] $msg\n";
|
echo '[' . date('Y-m-d H:i:s') . "] $msg\n";
|
||||||
if (function_exists('flush')) @flush();
|
|
||||||
|
if (function_exists('flush')) {
|
||||||
|
@flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
require __DIR__ . '/../vendor/autoload.php';
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
@ -27,28 +37,53 @@ require __DIR__ . '/../vendor/autoload.php';
|
|||||||
use PhpMqtt\Client\MqttClient;
|
use PhpMqtt\Client\MqttClient;
|
||||||
use PhpMqtt\Client\ConnectionSettings;
|
use PhpMqtt\Client\ConnectionSettings;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
// DB
|
// DB
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
$db = new PDO(
|
$db = new PDO(
|
||||||
"mysql:host=localhost;dbname=cagalhao;charset=utf8",
|
"mysql:host=localhost;dbname=cagalhao;charset=utf8",
|
||||||
"master",
|
"master",
|
||||||
"master",
|
"master",
|
||||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
[
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// RECOVERY BROKER
|
// =====================================================
|
||||||
|
// RECOVERY MQTT
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
$recoveryHost = 'mqtt-recovery.xupas.mywire.org';
|
$recoveryHost = 'mqtt-recovery.xupas.mywire.org';
|
||||||
$recoveryPort = 8884;
|
$recoveryPort = 8884;
|
||||||
|
|
||||||
$recoveryUser = 'recovery';
|
$recoveryUser = 'recovery';
|
||||||
$recoveryPass = 'recovery123';
|
$recoveryPass = 'recovery123';
|
||||||
|
|
||||||
// MAIN BROKER TEST
|
// =====================================================
|
||||||
|
// MAIN MQTT
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
$mainHost = 'mqtt.xupas.mywire.org';
|
$mainHost = 'mqtt.xupas.mywire.org';
|
||||||
$mainPort = 8883;
|
$mainPort = 8883;
|
||||||
|
|
||||||
// Cert principal a enviar aos ESPs
|
// =====================================================
|
||||||
$certPath = "/etc/mosquitto/certs/server.crt";
|
// CERT
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
$certPath = '/etc/mosquitto/certs/chain.pem'; // cert Let's Encrypt do broker principal
|
||||||
|
$certUrl = 'https://mqtt.xupas.mywire.org/cert.pem';
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// GLOBAL SECRET
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
$authSecret = 'K82A91F0';
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// MQTT SETTINGS
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
// MQTT recovery settings
|
|
||||||
$settings = (new ConnectionSettings())
|
$settings = (new ConnectionSettings())
|
||||||
->setUsername($recoveryUser)
|
->setUsername($recoveryUser)
|
||||||
->setPassword($recoveryPass)
|
->setPassword($recoveryPass)
|
||||||
@ -59,22 +94,38 @@ $settings = (new ConnectionSettings())
|
|||||||
->setSocketTimeout(1)
|
->setSocketTimeout(1)
|
||||||
->setReconnectAutomatically(true);
|
->setReconnectAutomatically(true);
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// MQTT CLIENT
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
$mqtt = new MqttClient(
|
$mqtt = new MqttClient(
|
||||||
$recoveryHost,
|
$recoveryHost,
|
||||||
$recoveryPort,
|
$recoveryPort,
|
||||||
'listener-recovery-' . getmypid()
|
'listener-recovery-' . getmypid()
|
||||||
);
|
);
|
||||||
|
|
||||||
function main_broker_online(string $host, int $port): bool {
|
// =====================================================
|
||||||
|
// MAIN BROKER CHECK
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
function main_broker_online(string $host, int $port): bool
|
||||||
|
{
|
||||||
$fp = @fsockopen($host, $port, $errno, $errstr, 2);
|
$fp = @fsockopen($host, $port, $errno, $errstr, 2);
|
||||||
|
|
||||||
if ($fp) {
|
if ($fp) {
|
||||||
fclose($fp);
|
fclose($fp);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function send_switch_primary(MqttClient $mqtt, PDO $db): void {
|
// =====================================================
|
||||||
|
// SEND SWITCH PRIMARY
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
function send_switch_primary(MqttClient $mqtt, PDO $db): void
|
||||||
|
{
|
||||||
$stmt = $db->query("
|
$stmt = $db->query("
|
||||||
SELECT uid
|
SELECT uid
|
||||||
FROM contadores
|
FROM contadores
|
||||||
@ -82,11 +133,14 @@ function send_switch_primary(MqttClient $mqtt, PDO $db): void {
|
|||||||
");
|
");
|
||||||
|
|
||||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
|
||||||
$uid = $row['uid'];
|
$uid = $row['uid'];
|
||||||
|
|
||||||
$mqtt->publish(
|
$mqtt->publish(
|
||||||
"esp/$uid/cmd",
|
"esp/$uid/cmd",
|
||||||
json_encode(['cmd' => 'switch_primary']),
|
json_encode([
|
||||||
|
'cmd' => 'SWITCH_PRIMARY'
|
||||||
|
]),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -94,7 +148,17 @@ function send_switch_primary(MqttClient $mqtt, PDO $db): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function send_cert_update(MqttClient $mqtt, string $uid, string $certPath): void {
|
// =====================================================
|
||||||
|
// SEND CERT
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
function send_cert_chunks(
|
||||||
|
MqttClient $mqtt,
|
||||||
|
string $uid,
|
||||||
|
string $certPath,
|
||||||
|
int $linesPerChunk = 16
|
||||||
|
): void {
|
||||||
|
|
||||||
if (!file_exists($certPath)) {
|
if (!file_exists($certPath)) {
|
||||||
logmsg("Cert não encontrado: $certPath", LOG_ERROR);
|
logmsg("Cert não encontrado: $certPath", LOG_ERROR);
|
||||||
return;
|
return;
|
||||||
@ -102,47 +166,317 @@ function send_cert_update(MqttClient $mqtt, string $uid, string $certPath): void
|
|||||||
|
|
||||||
$cert = file_get_contents($certPath);
|
$cert = file_get_contents($certPath);
|
||||||
|
|
||||||
|
// validar com openssl antes de enviar
|
||||||
|
$parsed = openssl_x509_read($cert);
|
||||||
|
if ($parsed === false) {
|
||||||
|
logmsg("Cert inválido (openssl): $certPath", LOG_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = explode("\n", rtrim($cert));
|
||||||
|
$chunks = array_chunk($lines, $linesPerChunk);
|
||||||
|
$total = count($chunks);
|
||||||
|
|
||||||
|
logmsg("CERT_BEGIN $uid total=$total chunks");
|
||||||
|
|
||||||
$mqtt->publish(
|
$mqtt->publish(
|
||||||
"esp/$uid/cmd",
|
"esp/$uid/cmd",
|
||||||
json_encode([
|
json_encode(['cmd' => 'CERT_BEGIN', 'total' => $total]),
|
||||||
'cmd' => 'UPDATE_CERT',
|
|
||||||
'pem' => $cert
|
|
||||||
]),
|
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
logmsg("UPDATE_CERT enviado para $uid");
|
foreach ($chunks as $seq => $chunk) {
|
||||||
|
$data = implode("\n", $chunk) . "\n";
|
||||||
|
$mqtt->publish(
|
||||||
|
"esp/$uid/cmd",
|
||||||
|
json_encode(['cmd' => 'CERT_CHUNK', 'seq' => $seq, 'data' => $data]),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
logmsg("CERT_CHUNK $uid seq=$seq");
|
||||||
|
usleep(100000); // 100ms entre chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
$mqtt->publish(
|
||||||
|
"esp/$uid/cmd",
|
||||||
|
json_encode(['cmd' => 'CERT_COMMIT']),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
logmsg("CERT_COMMIT enviado para $uid");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// SUBSCRIPTIONS
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
$subscribe = function () use (
|
$subscribe = function () use (
|
||||||
$mqtt,
|
$mqtt,
|
||||||
$db,
|
$db,
|
||||||
$certPath,
|
$certPath,
|
||||||
|
$certUrl,
|
||||||
$mainHost,
|
$mainHost,
|
||||||
$mainPort
|
$mainPort,
|
||||||
|
$authSecret
|
||||||
) {
|
) {
|
||||||
|
|
||||||
$mqtt->subscribe(
|
// =================================================
|
||||||
'esp/+/status',
|
// AUTH
|
||||||
function (string $topic, string $msg)
|
// =================================================
|
||||||
use ($db, $mqtt, $mainHost, $mainPort) {
|
|
||||||
|
|
||||||
$uid = explode('/', $topic)[1] ?? null;
|
$mqtt->subscribe(
|
||||||
if (!$uid) return;
|
'esp/+/auth',
|
||||||
|
function (string $topic, string $msg)
|
||||||
|
use ($db, $mqtt, $authSecret)
|
||||||
|
{
|
||||||
|
|
||||||
|
$uid = explode('/', $topic)[1] ?? null;
|
||||||
|
|
||||||
|
if (!$uid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logmsg("AUTH RX $uid => $msg");
|
||||||
|
|
||||||
|
$json = json_decode($msg, true);
|
||||||
|
|
||||||
|
if (!$json) {
|
||||||
|
|
||||||
|
logmsg(
|
||||||
|
"JSON AUTH inválido",
|
||||||
|
LOG_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// HELLO
|
||||||
|
// =============================================
|
||||||
|
|
||||||
|
if (($json['cmd'] ?? '') === 'HELLO') {
|
||||||
|
|
||||||
|
$nonce =
|
||||||
|
strtoupper(
|
||||||
|
bin2hex(random_bytes(16))
|
||||||
|
);
|
||||||
|
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
INSERT INTO contadores
|
INSERT INTO contadores
|
||||||
(uid, entradas, saidas, entradas_total, saidas_total, last_seen, online)
|
(uid, auth_nonce, last_seen, online, blocked)
|
||||||
VALUES
|
VALUES
|
||||||
(:uid,0,0,0,0,NOW(),1)
|
(:uid, :nonce, NOW(), 1, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
auth_nonce=:nonce,
|
||||||
|
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
|
||||||
|
]),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
logmsg(
|
||||||
|
"AUTH_CHALLENGE enviado para $uid"
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// AUTH RESPONSE
|
||||||
|
// =============================================
|
||||||
|
|
||||||
|
if (($json['cmd'] ?? '') === 'AUTH_RESPONSE') {
|
||||||
|
|
||||||
|
$hash =
|
||||||
|
strtoupper(
|
||||||
|
$json['hash'] ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$hash) {
|
||||||
|
|
||||||
|
logmsg(
|
||||||
|
"AUTH sem hash",
|
||||||
|
LOG_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT auth_nonce, blocked
|
||||||
|
FROM contadores
|
||||||
|
WHERE uid=:uid
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'uid' => $uid
|
||||||
|
]);
|
||||||
|
|
||||||
|
$row =
|
||||||
|
$stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
|
||||||
|
logmsg(
|
||||||
|
"UID desconhecido: $uid",
|
||||||
|
LOG_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nonce =
|
||||||
|
$row['auth_nonce'] ?? '';
|
||||||
|
|
||||||
|
if (!$nonce) {
|
||||||
|
|
||||||
|
logmsg(
|
||||||
|
"Nonce vazio para $uid",
|
||||||
|
LOG_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expected =
|
||||||
|
strtoupper(
|
||||||
|
hash_hmac(
|
||||||
|
'sha256',
|
||||||
|
$nonce,
|
||||||
|
$authSecret
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hash_equals($expected, $hash)) {
|
||||||
|
|
||||||
|
// verificar se está aprovado
|
||||||
|
if ((int)($row['blocked'] ?? 1) === 1) {
|
||||||
|
|
||||||
|
logmsg("AUTH PENDENTE/BLOQUEADO $uid — aguarda aprovação", LOG_ERROR);
|
||||||
|
|
||||||
|
$mqtt->publish(
|
||||||
|
"esp/$uid/cmd",
|
||||||
|
json_encode(['cmd' => 'AUTH_FAIL']),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logmsg("AUTH OK $uid");
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE contadores
|
||||||
|
SET
|
||||||
|
auth_nonce=NULL,
|
||||||
|
auth_ok=1,
|
||||||
|
online=1,
|
||||||
|
last_seen=NOW()
|
||||||
|
WHERE uid=:uid
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'uid' => $uid
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mqtt->publish(
|
||||||
|
"esp/$uid/cmd",
|
||||||
|
json_encode([
|
||||||
|
'cmd' => 'AUTH_OK'
|
||||||
|
]),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
logmsg(
|
||||||
|
"AUTH FAIL $uid",
|
||||||
|
LOG_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
$mqtt->publish(
|
||||||
|
"esp/$uid/cmd",
|
||||||
|
json_encode([
|
||||||
|
'cmd' => 'AUTH_FAIL'
|
||||||
|
]),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
// =================================================
|
||||||
|
// STATUS
|
||||||
|
// =================================================
|
||||||
|
|
||||||
|
$mqtt->subscribe(
|
||||||
|
'esp/+/status',
|
||||||
|
function (
|
||||||
|
string $topic,
|
||||||
|
string $msg
|
||||||
|
) use (
|
||||||
|
$db,
|
||||||
|
$mqtt,
|
||||||
|
$mainHost,
|
||||||
|
$mainPort
|
||||||
|
) {
|
||||||
|
|
||||||
|
$uid = explode('/', $topic)[1] ?? null;
|
||||||
|
|
||||||
|
if (!$uid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO contadores
|
||||||
|
(
|
||||||
|
uid,
|
||||||
|
entradas,
|
||||||
|
saidas,
|
||||||
|
entradas_total,
|
||||||
|
saidas_total,
|
||||||
|
last_seen,
|
||||||
|
online
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
:uid,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
NOW(),
|
||||||
|
1
|
||||||
|
)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
last_seen=NOW(),
|
last_seen=NOW(),
|
||||||
online=1
|
online=1
|
||||||
");
|
");
|
||||||
|
|
||||||
$stmt->execute(['uid' => $uid]);
|
$stmt->execute([
|
||||||
|
'uid' => $uid
|
||||||
|
]);
|
||||||
|
|
||||||
logmsg("RECOVERY STATUS $uid => $msg");
|
logmsg(
|
||||||
|
"RECOVERY STATUS $uid => $msg"
|
||||||
|
);
|
||||||
|
|
||||||
static $lastCheck = 0;
|
static $lastCheck = 0;
|
||||||
|
|
||||||
@ -152,15 +486,27 @@ $subscribe = function () use (
|
|||||||
|
|
||||||
logmsg("CHECK broker");
|
logmsg("CHECK broker");
|
||||||
|
|
||||||
if (main_broker_online($mainHost, $mainPort)) {
|
if (
|
||||||
|
main_broker_online(
|
||||||
|
$mainHost,
|
||||||
|
$mainPort
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
logmsg("MAIN broker online");
|
logmsg(
|
||||||
|
"MAIN broker online"
|
||||||
|
);
|
||||||
|
|
||||||
send_switch_primary($mqtt, $db);
|
send_switch_primary(
|
||||||
|
$mqtt,
|
||||||
|
$db
|
||||||
|
);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
logmsg("MAIN broker offline");
|
logmsg(
|
||||||
|
"MAIN broker offline"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,61 +514,140 @@ $subscribe = function () use (
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// =================================================
|
||||||
|
// EVENT
|
||||||
|
// =================================================
|
||||||
|
|
||||||
$mqtt->subscribe('esp/+/event', function (string $topic, string $msg) use ($db) {
|
$mqtt->subscribe(
|
||||||
$uid = explode('/', $topic)[1] ?? null;
|
'esp/+/event',
|
||||||
if (!$uid) return;
|
function (
|
||||||
|
string $topic,
|
||||||
|
string $msg
|
||||||
|
) {
|
||||||
|
|
||||||
logmsg("RECOVERY EVENT $uid => $msg");
|
$uid =
|
||||||
}, 0);
|
explode('/', $topic)[1] ?? null;
|
||||||
|
|
||||||
$mqtt->subscribe('esp/+/cert_request', function (string $topic, string $msg) use ($mqtt, $certPath) {
|
if (!$uid) {
|
||||||
$uid = explode('/', $topic)[1] ?? null;
|
return;
|
||||||
if (!$uid) return;
|
}
|
||||||
|
|
||||||
logmsg("CERT_REQUEST de $uid");
|
logmsg(
|
||||||
send_cert_update($mqtt, $uid, $certPath);
|
"RECOVERY EVENT $uid => $msg"
|
||||||
}, 0);
|
);
|
||||||
|
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// =================================================
|
||||||
|
// CERT REQUEST
|
||||||
|
// =================================================
|
||||||
|
|
||||||
|
$mqtt->subscribe(
|
||||||
|
'esp/+/cert_request',
|
||||||
|
function (
|
||||||
|
string $topic,
|
||||||
|
string $msg
|
||||||
|
) use (
|
||||||
|
$mqtt,
|
||||||
|
$certPath,
|
||||||
|
$certUrl
|
||||||
|
) {
|
||||||
|
|
||||||
|
$uid =
|
||||||
|
explode('/', $topic)[1] ?? null;
|
||||||
|
|
||||||
|
if (!$uid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logmsg("CERT_REQUEST de $uid → chunks");
|
||||||
|
|
||||||
|
send_cert_chunks($mqtt, $uid, $certPath);
|
||||||
|
|
||||||
|
},
|
||||||
|
1
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// MAIN LOOP
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logmsg("Ligar recovery $recoveryHost:$recoveryPort");
|
|
||||||
|
logmsg(
|
||||||
|
"Ligar recovery $recoveryHost:$recoveryPort"
|
||||||
|
);
|
||||||
|
|
||||||
$mqtt->connect($settings, false);
|
$mqtt->connect($settings, false);
|
||||||
|
|
||||||
$subscribe();
|
$subscribe();
|
||||||
|
|
||||||
logmsg("RECOVERY ligado e subscrito");
|
logmsg(
|
||||||
|
"RECOVERY ligado e subscrito"
|
||||||
|
);
|
||||||
|
|
||||||
$nextMainCheck = time() + 10;
|
$nextMainCheck = time() + 10;
|
||||||
|
|
||||||
while ($mqtt->isConnected()) {
|
while ($mqtt->isConnected()) {
|
||||||
|
|
||||||
$mqtt->loop(false);
|
$mqtt->loop(false);
|
||||||
logmsg("tick");
|
|
||||||
if (time() >= $nextMainCheck) {
|
if (time() >= $nextMainCheck) {
|
||||||
|
|
||||||
$nextMainCheck = time() + 15;
|
$nextMainCheck = time() + 15;
|
||||||
|
|
||||||
logmsg("LOOP recovery", LOG_DEBUG);
|
logmsg(
|
||||||
|
"LOOP recovery",
|
||||||
|
LOG_DEBUG
|
||||||
|
);
|
||||||
|
|
||||||
if (main_broker_online($mainHost, $mainPort)) {
|
if (
|
||||||
|
main_broker_online(
|
||||||
|
$mainHost,
|
||||||
|
$mainPort
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
logmsg("MAIN broker online");
|
logmsg(
|
||||||
|
"MAIN broker online"
|
||||||
|
);
|
||||||
|
|
||||||
|
logmsg(
|
||||||
|
"Mandar switch_primary"
|
||||||
|
);
|
||||||
|
|
||||||
|
send_switch_primary(
|
||||||
|
$mqtt,
|
||||||
|
$db
|
||||||
|
);
|
||||||
|
|
||||||
logmsg("MAIN broker online. Mandar switch_primary.");
|
|
||||||
send_switch_primary($mqtt, $db);
|
|
||||||
} else {
|
} else {
|
||||||
logmsg("MAIN broker ainda offline", LOG_DEBUG);
|
|
||||||
|
logmsg(
|
||||||
|
"MAIN broker ainda offline",
|
||||||
|
LOG_DEBUG
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
usleep(100000);
|
usleep(100000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$mqtt->disconnect();
|
$mqtt->disconnect();
|
||||||
|
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
logmsg("ERRO recovery: " . $e->getMessage(), LOG_ERROR);
|
|
||||||
|
logmsg(
|
||||||
|
"ERRO recovery: " .
|
||||||
|
$e->getMessage(),
|
||||||
|
LOG_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
sleep(5);
|
sleep(5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,6 +92,9 @@ if (isset($_GET['vpn'])) {
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<a href="../console.php" class="btn btn-outline-light w-100">📊 Estado dos Dispositivos</a>
|
<a href="../console.php" class="btn btn-outline-light w-100">📊 Estado dos Dispositivos</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<a href="../devices.php" class="btn btn-outline-warning w-100">🔌 Gerir Dispositivos</a>
|
||||||
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<a href="../comandos.php" class="btn btn-outline-light w-100">🛠️ Enviar Comando</a>
|
<a href="../comandos.php" class="btn btn-outline-light w-100">🛠️ Enviar Comando</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -63,6 +63,8 @@ $mqttHost = 'mqtt.xupas.mywire.org';
|
|||||||
$mqttUser = 'xupa';
|
$mqttUser = 'xupa';
|
||||||
$mqttPass = 'xupa';
|
$mqttPass = 'xupa';
|
||||||
|
|
||||||
|
$deviceSecret = 'K82A91F0';
|
||||||
|
|
||||||
$bootstrapSecret = "XUPA_TEMP_SECRET_2026";
|
$bootstrapSecret = "XUPA_TEMP_SECRET_2026";
|
||||||
$certPath = "/etc/mosquitto/certs/server.crt";
|
$certPath = "/etc/mosquitto/certs/server.crt";
|
||||||
|
|
||||||
@ -98,24 +100,70 @@ $mqttBootstrap = new MqttClient($mqttHost, 1883, 'listener-bootstrap');
|
|||||||
/* =========================
|
/* =========================
|
||||||
* SUBSCRIPTIONS TLS
|
* SUBSCRIPTIONS TLS
|
||||||
* ========================= */
|
* ========================= */
|
||||||
$subscribeTls = function () use ($mqttTls, $db) {
|
$subscribeTls = function () use ($mqttTls, $db, $deviceSecret) {
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// STATUS ESP
|
// STATUS ESP
|
||||||
// =========================
|
// =========================
|
||||||
$mqttTls->subscribe('esp/+/status', function (string $topic, string $msg) use ($db) {
|
$mqttTls->subscribe('esp/+/status', function (string $topic, string $msg)
|
||||||
|
use ($db, $mqttTls, $deviceSecret)
|
||||||
|
{
|
||||||
$uid = explode('/', $topic)[1] ?? null;
|
$uid = explode('/', $topic)[1] ?? null;
|
||||||
if (!$uid) return;
|
if (!$uid) return;
|
||||||
|
|
||||||
$stmt = $db->prepare("
|
$json = json_decode($msg, true);
|
||||||
INSERT INTO contadores (uid, entradas, saidas, entradas_total, saidas_total, last_seen, online)
|
$uptime = isset($json['uptime']) ? (int)$json['uptime'] : null;
|
||||||
VALUES (:uid,0,0,0,0,NOW(),1)
|
$heap = isset($json['heap']) ? (int)$json['heap'] : null;
|
||||||
ON DUPLICATE KEY UPDATE last_seen=NOW(), online=1
|
$rssi = isset($json['rssi']) ? (int)$json['rssi'] : null;
|
||||||
");
|
$broker = isset($json['broker']) ? (string)$json['broker'] : null;
|
||||||
$stmt->execute(['uid' => $uid]);
|
$hmac = isset($json['hmac']) ? strtoupper((string)$json['hmac']) : null;
|
||||||
|
|
||||||
logmsg("STATUS $uid");
|
// Validar HMAC — apenas aceitar heartbeats assinados
|
||||||
|
if ($hmac === null || $uptime === null) {
|
||||||
|
logmsg("STATUS sem HMAC/uptime $uid — ignorado", LOG_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expected = strtoupper(hash_hmac('sha256', $uid . '|' . $uptime, $deviceSecret));
|
||||||
|
|
||||||
|
if (!hash_equals($expected, $hmac)) {
|
||||||
|
logmsg("STATUS HMAC INVÁLIDO $uid — ignorado", LOG_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HMAC OK → actualizar DB
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO contadores
|
||||||
|
(uid, entradas, saidas, entradas_total, saidas_total, last_seen, online,
|
||||||
|
uptime, heap, rssi, current_broker)
|
||||||
|
VALUES
|
||||||
|
(:uid, 0, 0, 0, 0, NOW(), 1,
|
||||||
|
:uptime, :heap, :rssi, :broker)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
last_seen=NOW(),
|
||||||
|
online=1,
|
||||||
|
uptime=COALESCE(:uptime2, uptime),
|
||||||
|
heap=COALESCE(:heap2, heap),
|
||||||
|
rssi=COALESCE(:rssi2, rssi),
|
||||||
|
current_broker=COALESCE(:broker2, current_broker)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'uid' => $uid,
|
||||||
|
'uptime' => $uptime,
|
||||||
|
'heap' => $heap,
|
||||||
|
'rssi' => $rssi,
|
||||||
|
'broker' => $broker,
|
||||||
|
'uptime2' => $uptime,
|
||||||
|
'heap2' => $heap,
|
||||||
|
'rssi2' => $rssi,
|
||||||
|
'broker2' => $broker,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Responder com HEARTBEAT_OK → ESP arranca serviços (sem retain)
|
||||||
|
$mqttTls->publish("esp/$uid/cmd",
|
||||||
|
json_encode(['cmd' => 'HEARTBEAT_OK']), 1, false);
|
||||||
|
|
||||||
|
logmsg("STATUS OK $uid uptime=$uptime heap=$heap rssi=$rssi broker=$broker → HEARTBEAT_OK");
|
||||||
|
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
@ -217,11 +265,28 @@ while (true) {
|
|||||||
$subscribeBootstrap();
|
$subscribeBootstrap();
|
||||||
logmsg("Bootstrap ligado");
|
logmsg("Bootstrap ligado");
|
||||||
|
|
||||||
|
$nextOfflineCheck = time() + 15;
|
||||||
|
|
||||||
while ($mqttTls->isConnected() && $mqttBootstrap->isConnected()) {
|
while ($mqttTls->isConnected() && $mqttBootstrap->isConnected()) {
|
||||||
|
|
||||||
$mqttTls->loop(true, false);
|
$mqttTls->loop(true, false);
|
||||||
$mqttBootstrap->loop(true, false);
|
$mqttBootstrap->loop(true, false);
|
||||||
|
|
||||||
|
if (time() >= $nextOfflineCheck) {
|
||||||
|
$nextOfflineCheck = time() + 15;
|
||||||
|
|
||||||
|
$affected = $db->exec("
|
||||||
|
UPDATE contadores
|
||||||
|
SET online=0
|
||||||
|
WHERE online=1
|
||||||
|
AND TIMESTAMPDIFF(SECOND, last_seen, NOW()) > 35
|
||||||
|
");
|
||||||
|
|
||||||
|
if ($affected > 0) {
|
||||||
|
logmsg("OFFLINE sweep: $affected dispositivo(s) marcado(s) offline");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
usleep(100000);
|
usleep(100000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,6 @@ if (session_status() === PHP_SESSION_NONE) {
|
|||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
if (!isset($_SESSION['user_id'])) {
|
if (!isset($_SESSION['user_id'])) {
|
||||||
header("Location: login.php");
|
header("Location: /api/login.php");
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@ -27,7 +27,7 @@ mqtt_publish($topic, json_encode($msg, JSON_UNESCAPED_SLASHES));
|
|||||||
|
|
||||||
// ----------------------- BD -------------------------
|
// ----------------------- BD -------------------------
|
||||||
if ($cmd === 'BLOCK') {
|
if ($cmd === 'BLOCK') {
|
||||||
$stmt = $db->prepare("UPDATE contadores SET blocked = ? WHERE uid = ?");
|
$stmt = $pdo->prepare("UPDATE contadores SET blocked = ? WHERE uid = ?");
|
||||||
$stmt->execute([intval($value), $uid]);
|
$stmt->execute([intval($value), $uid]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ try {
|
|||||||
$_SESSION['username'] = $user['username'];
|
$_SESSION['username'] = $user['username'];
|
||||||
|
|
||||||
// Redireciona para o menu principal
|
// Redireciona para o menu principal
|
||||||
header("Location: menu.php");
|
header("Location: /api/menu.php");
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Buscar lista de devices
|
// Buscar lista de devices
|
||||||
$devices = $pdo->query("SELECT uid FROM devices ORDER BY uid ASC")->fetchAll();
|
$devices = $pdo->query("SELECT uid FROM contadores ORDER BY uid ASC")->fetchAll();
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
die("Erro: " . $e->getMessage());
|
die("Erro: " . $e->getMessage());
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
return [
|
return [
|
||||||
'host' => '192.168.10.200', // depois trocas para 'estica.bot.nu'
|
'host' => 'mqtt.xupas.mywire.org',
|
||||||
'port' => 1883, // 8883 se fores usar TLS mais tarde
|
'port' => 8883,
|
||||||
'clientId' => 'php-web-' . bin2hex(random_bytes(3)),
|
'clientId' => 'php-web-' . bin2hex(random_bytes(3)),
|
||||||
'username' => 'xupa',
|
'username' => 'xupa',
|
||||||
'password' => 'xupa',
|
'password' => 'xupa',
|
||||||
|
|||||||
638
console.php
638
console.php
@ -1,199 +1,482 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . "/api/protecao.php";
|
||||||
require __DIR__ . "/api/db.php";
|
require __DIR__ . "/api/db.php";
|
||||||
|
|
||||||
// ---------- SORT ----------
|
// =====================================================
|
||||||
$sort = $_GET['sort'] ?? 'state';
|
// GUARDAR NOTA
|
||||||
$orderSql = ($sort === 'uid')
|
// =====================================================
|
||||||
? "ORDER BY uid ASC"
|
|
||||||
: "ORDER BY online DESC, uid ASC";
|
|
||||||
|
|
||||||
// ---------- OFFLINE AUTOMÁTICO (20s) ----------
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_note'])) {
|
||||||
$pdo->query("
|
|
||||||
UPDATE contadores
|
$uid = $_POST['uid'] ?? '';
|
||||||
SET online = 0
|
$note = trim($_POST['note'] ?? '');
|
||||||
WHERE TIMESTAMPDIFF(SECOND, last_seen, NOW()) > 20
|
|
||||||
");
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE contadores
|
||||||
|
SET note = ?
|
||||||
|
WHERE uid = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([$note, $uid]);
|
||||||
|
|
||||||
// ---------- BLOQUEIO ----------
|
|
||||||
if (isset($_GET['toggle'])) {
|
|
||||||
$uid = $_GET['toggle'];
|
|
||||||
$pdo->prepare("UPDATE contadores SET blocked = 1 - blocked WHERE uid = ?")
|
|
||||||
->execute([$uid]);
|
|
||||||
header("Location: console.php");
|
header("Location: console.php");
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- PRIMEIRA QUERY (para totals) ----------
|
// =====================================================
|
||||||
|
// 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 = "
|
$sql = "
|
||||||
SELECT uid, entradas, saidas, online, blocked,
|
SELECT
|
||||||
last_seen,
|
uid,
|
||||||
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
note,
|
||||||
|
entradas,
|
||||||
|
saidas,
|
||||||
|
online,
|
||||||
|
blocked,
|
||||||
|
current_broker,
|
||||||
|
rssi,
|
||||||
|
heap,
|
||||||
|
uptime,
|
||||||
|
last_seen,
|
||||||
|
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
||||||
FROM contadores
|
FROM contadores
|
||||||
$orderSql
|
$orderSql
|
||||||
";
|
";
|
||||||
|
|
||||||
$stmt = $pdo->query($sql);
|
$stmt = $pdo->query($sql);
|
||||||
|
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// Totais
|
// =====================================================
|
||||||
|
// TOTAIS
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
$sumEntradas = 0;
|
$sumEntradas = 0;
|
||||||
$sumSaidas = 0;
|
$sumSaidas = 0;
|
||||||
$onlineCount = 0;
|
|
||||||
|
$onlineCount = 0;
|
||||||
$offlineCount = 0;
|
$offlineCount = 0;
|
||||||
|
|
||||||
foreach ($rows as $r) {
|
foreach ($rows as $r) {
|
||||||
|
|
||||||
$sumEntradas += (int)$r['entradas'];
|
$sumEntradas += (int)$r['entradas'];
|
||||||
$sumSaidas += (int)$r['saidas'];
|
$sumSaidas += (int)$r['saidas'];
|
||||||
if ($r['online'] == 1) $onlineCount++;
|
|
||||||
else $offlineCount++;
|
if ((int)$r['online'] === 1) {
|
||||||
|
$onlineCount++;
|
||||||
|
} else {
|
||||||
|
$offlineCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="pt" data-bs-theme="dark">
|
<html lang="pt" data-bs-theme="dark">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Painel de Contadores</title>
|
|
||||||
|
<title>MQTT Swarm Console</title>
|
||||||
|
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #0f0f0f;
|
background: #0b0b0b;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: #1a1a1a;
|
background: #151515;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
.table-striped > tbody > tr:nth-of-type(odd) {
|
.table-striped > tbody > tr:nth-of-type(odd) {
|
||||||
background-color: #1c1c1c;
|
background-color: #171717;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-striped > tbody > tr:nth-of-type(even) {
|
.table-striped > tbody > tr:nth-of-type(even) {
|
||||||
background-color: #151515;
|
background-color: #101010;
|
||||||
}
|
}
|
||||||
.table thead {
|
|
||||||
background-color: #222;
|
.table-hover tbody tr:hover {
|
||||||
|
background-color: #202020;
|
||||||
}
|
}
|
||||||
h1 {
|
|
||||||
color: #f0f0f0;
|
.note-input {
|
||||||
|
min-width: 220px;
|
||||||
|
background: #111;
|
||||||
|
border: 1px solid #333;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
a {
|
|
||||||
color: #58a6ff;
|
.note-input:focus {
|
||||||
|
background: #181818;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #58a6ff;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
a:hover {
|
|
||||||
color: #8cc5ff;
|
.pulse-cell {
|
||||||
|
animation: pulse 1s infinite;
|
||||||
}
|
}
|
||||||
.btn-danger {
|
|
||||||
background-color: #a32424;
|
@keyframes pulse {
|
||||||
border-color: #6b0000;
|
|
||||||
|
0% {
|
||||||
|
background-color: rgba(0,255,0,0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-color: rgba(0,255,0,0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-color: rgba(0,255,0,0.03);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.btn-success {
|
|
||||||
background-color: #1f6f33;
|
.header-title {
|
||||||
border-color: #0f4d1f;
|
color: #00ffcc;
|
||||||
|
text-shadow: 0 0 8px rgba(0,255,180,0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.small-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="container py-4">
|
<div class="container-fluid py-4">
|
||||||
|
|
||||||
<h1 class="mb-4">Painel de Contadores</h1>
|
<h1 class="mb-4 header-title">
|
||||||
|
🌋 MQTT SWARM CONSOLE
|
||||||
|
</h1>
|
||||||
|
|
||||||
<!-- RESUMO -->
|
<!-- RESUMO -->
|
||||||
<div class="row mb-4 text-center">
|
<div class="row mb-4 text-center">
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="p-3 card rounded">
|
<div class="p-3 card rounded">
|
||||||
<h4><?= $onlineCount ?></h4>
|
<h4 id="total_online"><?= $onlineCount ?></h4>
|
||||||
<div class="text-success fw-bold">Online</div>
|
<div class="text-success fw-bold">
|
||||||
|
🟢 ONLINE
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="p-3 card rounded">
|
<div class="p-3 card rounded">
|
||||||
<h4><?= $offlineCount ?></h4>
|
<h4 id="total_offline"><?= $offlineCount ?></h4>
|
||||||
<div class="text-danger fw-bold">Offline</div>
|
<div class="text-danger fw-bold">
|
||||||
|
🔴 OFFLINE
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="p-3 card rounded">
|
<div class="p-3 card rounded">
|
||||||
<h4><?= $sumEntradas ?></h4>
|
<h4 id="total_entradas"><?= $sumEntradas ?></h4>
|
||||||
<div class="fw-bold">Entradas</div>
|
<div class="fw-bold">
|
||||||
|
💰 ENTRADAS
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="p-3 card rounded">
|
<div class="p-3 card rounded">
|
||||||
<h4><?= $sumSaidas ?></h4>
|
<h4 id="total_saidas"><?= $sumSaidas ?></h4>
|
||||||
<div class="fw-bold">Saídas</div>
|
<div class="fw-bold">
|
||||||
|
💸 SAÍDAS
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TABELA -->
|
<!-- TABELA -->
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<table class="table table-striped table-hover align-middle">
|
<table class="table table-striped table-hover align-middle">
|
||||||
|
|
||||||
<thead>
|
<thead>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th><a href="?sort=uid">UID</a></th>
|
|
||||||
<th>Entradas</th>
|
<th>UID</th>
|
||||||
<th>Saídas</th>
|
<th>NOTA</th>
|
||||||
<th><a href="?sort=state">Estado</a></th>
|
<th>ENTRADAS</th>
|
||||||
<th>Último Sinal</th>
|
<th>SAÍDAS</th>
|
||||||
<th>Ações</th>
|
<th>ESTADO</th>
|
||||||
|
<th>BROKER</th>
|
||||||
|
<th>RSSI</th>
|
||||||
|
<th>HEAP</th>
|
||||||
|
<th>UPTIME</th>
|
||||||
|
<th>ÚLTIMO</th>
|
||||||
|
<th>AÇÕES</th>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody id="tabela_corpo">
|
<tbody id="tabela_corpo">
|
||||||
|
|
||||||
<?php foreach ($rows as $r): ?>
|
<?php foreach ($rows as $r): ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$online = (int)$r['online'] === 1;
|
|
||||||
$blocked = (int)$r['blocked'] === 1;
|
|
||||||
|
|
||||||
if ($blocked) {
|
$online = (int)$r['online'] === 1;
|
||||||
$state = '<span class="badge bg-danger">Bloqueado</span>';
|
$blocked = (int)$r['blocked'] === 1;
|
||||||
} elseif ($online) {
|
|
||||||
$state = '<span class="badge bg-success">Online</span>';
|
if ($blocked) {
|
||||||
} else {
|
|
||||||
$state = '<span class="badge bg-secondary">Offline</span>';
|
$state =
|
||||||
}
|
'<span class="badge bg-danger">
|
||||||
|
BLOQUEADO
|
||||||
|
</span>';
|
||||||
|
|
||||||
|
} elseif ($online) {
|
||||||
|
|
||||||
|
$state =
|
||||||
|
'<span class="badge bg-success">
|
||||||
|
ONLINE
|
||||||
|
</span>';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
$state =
|
||||||
|
'<span class="badge bg-secondary">
|
||||||
|
OFFLINE
|
||||||
|
</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($r['current_broker'] ?? '') === 'recovery') {
|
||||||
|
|
||||||
|
$broker =
|
||||||
|
'<span class="badge bg-warning text-dark">
|
||||||
|
RECOVERY
|
||||||
|
</span>';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
$broker =
|
||||||
|
'<span class="badge bg-primary">
|
||||||
|
PRIMARY
|
||||||
|
</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$ago = (int)$r['ago'];
|
||||||
|
|
||||||
|
$lastSeen = ($ago < 60)
|
||||||
|
? "{$ago}s"
|
||||||
|
: round($ago / 60) . "m";
|
||||||
|
|
||||||
|
$pulseClass =
|
||||||
|
($ago <= 2 && $online)
|
||||||
|
? "pulse-cell"
|
||||||
|
: "";
|
||||||
|
|
||||||
$ago = (int)$r['ago'];
|
|
||||||
$lastSeen = ($ago < 60)
|
|
||||||
? "{$ago}s"
|
|
||||||
: round($ago / 60) . "m";
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<tr>
|
<tr class="<?= $pulseClass ?>">
|
||||||
<td class="fw-bold"><?= htmlspecialchars($r['uid']) ?></td>
|
|
||||||
<td><?= $r['entradas'] ?></td>
|
<td class="fw-bold">
|
||||||
<td><?= $r['saidas'] ?></td>
|
<?= htmlspecialchars($r['uid']) ?>
|
||||||
<td><?= $state ?></td>
|
|
||||||
<td><?= $lastSeen ?></td>
|
|
||||||
<td>
|
|
||||||
<a href="?toggle=<?= urlencode($r['uid']) ?>"
|
|
||||||
class="btn btn-sm <?= $blocked ? 'btn-success' : 'btn-danger' ?>">
|
|
||||||
<?= $blocked ? 'Desbloquear' : 'Bloquear' ?>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="uid"
|
||||||
|
value="<?= htmlspecialchars($r['uid']) ?>">
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
|
||||||
|
<input type="text"
|
||||||
|
name="note"
|
||||||
|
value="<?= htmlspecialchars($r['note'] ?? '') ?>"
|
||||||
|
class="form-control form-control-sm note-input"
|
||||||
|
placeholder="nota...">
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
name="save_note"
|
||||||
|
class="btn btn-sm btn-primary">
|
||||||
|
|
||||||
|
Save
|
||||||
|
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td><?= (int)$r['entradas'] ?></td>
|
||||||
|
|
||||||
|
<td><?= (int)$r['saidas'] ?></td>
|
||||||
|
|
||||||
|
<td><?= $state ?></td>
|
||||||
|
|
||||||
|
<td><?= $broker ?></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
|
<?= isset($r['rssi'])
|
||||||
|
? $r['rssi'] . " dBm"
|
||||||
|
: "-" ?>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
|
<?= isset($r['heap'])
|
||||||
|
? number_format((int)$r['heap'])
|
||||||
|
: "-" ?>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
if (isset($r['uptime'])) {
|
||||||
|
|
||||||
|
$up = (int)$r['uptime'];
|
||||||
|
|
||||||
|
echo gmdate("H:i:s", $up);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
echo "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td><?= $lastSeen ?></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
|
||||||
|
<a href="?toggle=<?= urlencode($r['uid']) ?>"
|
||||||
|
class="btn btn-sm <?= $blocked ? 'btn-success' : 'btn-danger' ?>">
|
||||||
|
|
||||||
|
<?= $blocked
|
||||||
|
? 'Desbloquear'
|
||||||
|
: 'Bloquear' ?>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="?remove=<?= urlencode($r['uid']) ?>"
|
||||||
|
class="btn btn-sm btn-warning"
|
||||||
|
onclick="return confirm('Remover device?')">
|
||||||
|
|
||||||
|
Remover
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ------------ AUTO-REFRESH AJAX ---------------- -->
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshTable() {
|
async function refreshTable() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/contadores_json.php?x=' + Date.now());
|
|
||||||
|
const res =
|
||||||
|
await fetch(
|
||||||
|
'/api/contadores_json.php?x='
|
||||||
|
+ Date.now()
|
||||||
|
);
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
@ -201,62 +484,173 @@ async function refreshTable() {
|
|||||||
|
|
||||||
for (const r of data.rows) {
|
for (const r of data.rows) {
|
||||||
|
|
||||||
// Estado
|
let state = "";
|
||||||
// Estado baseado APENAS no valor online vindo da DB
|
|
||||||
let state = "";
|
|
||||||
|
|
||||||
if (r.blocked == 1) {
|
if (r.blocked == 1) {
|
||||||
state = '<span class="badge bg-danger">Bloqueado</span>';
|
|
||||||
}
|
|
||||||
else if (r.online == 1) {
|
|
||||||
state = '<span class="badge bg-success">Online</span>';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state = '<span class="badge bg-secondary">Offline</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tempo
|
state =
|
||||||
let lastSeen = (r.ago < 60) ? `${r.ago}s` : `${Math.round(r.ago/60)}m`;
|
'<span class="badge bg-danger">BLOQUEADO</span>';
|
||||||
|
|
||||||
// Animação pulse quando r.ago <= 2
|
} else if (r.online == 1) {
|
||||||
let pulseClass = (r.ago <= 2 && r.online == 1) ? "pulse-cell" : "";
|
|
||||||
|
state =
|
||||||
|
'<span class="badge bg-success">ONLINE</span>';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
state =
|
||||||
|
'<span class="badge bg-secondary">OFFLINE</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
let broker = "";
|
||||||
|
|
||||||
|
if (r.current_broker === 'recovery') {
|
||||||
|
|
||||||
|
broker =
|
||||||
|
'<span class="badge bg-warning text-dark">RECOVERY</span>';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
broker =
|
||||||
|
'<span class="badge bg-primary">PRIMARY</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastSeen =
|
||||||
|
(r.ago < 60)
|
||||||
|
? `${r.ago}s`
|
||||||
|
: `${Math.round(r.ago / 60)}m`;
|
||||||
|
|
||||||
|
let pulseClass =
|
||||||
|
(r.ago <= 2 && r.online == 1)
|
||||||
|
? "pulse-cell"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
let uptime = "-";
|
||||||
|
|
||||||
|
if (r.uptime && !isNaN(r.uptime)) {
|
||||||
|
|
||||||
|
let sec = parseInt(r.uptime);
|
||||||
|
|
||||||
|
let h =
|
||||||
|
String(Math.floor(sec / 3600))
|
||||||
|
.padStart(2, '0');
|
||||||
|
|
||||||
|
let m =
|
||||||
|
String(Math.floor((sec % 3600) / 60))
|
||||||
|
.padStart(2, '0');
|
||||||
|
|
||||||
|
let s =
|
||||||
|
String(sec % 60)
|
||||||
|
.padStart(2, '0');
|
||||||
|
|
||||||
|
uptime = `${h}:${m}:${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<tr class="${pulseClass}">
|
<tr class="${esc(pulseClass)}">
|
||||||
<td class="fw-bold">${r.uid}</td>
|
|
||||||
<td>${r.entradas}</td>
|
<td class="fw-bold">
|
||||||
<td>${r.saidas}</td>
|
${esc(r.uid)}
|
||||||
<td>${state}</td>
|
|
||||||
<td>${lastSeen}</td>
|
|
||||||
<td>
|
|
||||||
<a href="?toggle=${r.uid}"
|
|
||||||
class="btn btn-sm ${r.blocked ? 'btn-success' : 'btn-danger'}">
|
|
||||||
${r.blocked ? 'Desbloquear' : 'Bloquear'}
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="uid"
|
||||||
|
value="${esc(r.uid)}">
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
|
||||||
|
<input type="text"
|
||||||
|
name="note"
|
||||||
|
value="${esc(r.note)}"
|
||||||
|
class="form-control form-control-sm note-input"
|
||||||
|
placeholder="nota...">
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
name="save_note"
|
||||||
|
class="btn btn-sm btn-primary">
|
||||||
|
|
||||||
|
Save
|
||||||
|
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>${esc(r.entradas)}</td>
|
||||||
|
|
||||||
|
<td>${esc(r.saidas)}</td>
|
||||||
|
|
||||||
|
<td>${state}</td>
|
||||||
|
|
||||||
|
<td>${broker}</td>
|
||||||
|
|
||||||
|
<td>${r.rssi != null ? esc(r.rssi) + ' dBm' : '-'}</td>
|
||||||
|
|
||||||
|
<td>${r.heap != null ? Number(r.heap).toLocaleString() : '-'}</td>
|
||||||
|
|
||||||
|
<td>${uptime}</td>
|
||||||
|
|
||||||
|
<td>${esc(lastSeen)}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
|
||||||
|
<a href="?toggle=${esc(r.uid)}"
|
||||||
|
class="btn btn-sm ${r.blocked ? 'btn-success' : 'btn-danger'}">
|
||||||
|
|
||||||
|
${r.blocked ? 'Desbloquear' : 'Bloquear'}
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="?remove=${esc(r.uid)}"
|
||||||
|
class="btn btn-sm btn-warning"
|
||||||
|
onclick="return confirm('Remover device?')">
|
||||||
|
|
||||||
|
Remover
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("tabela_corpo").innerHTML = html;
|
document.getElementById("tabela_corpo").innerHTML = html;
|
||||||
|
|
||||||
// Atualizar totais
|
document.getElementById("total_online").innerText =
|
||||||
document.getElementById("total_online").innerText = data.online;
|
data.online;
|
||||||
document.getElementById("total_offline").innerText = data.offline;
|
|
||||||
document.getElementById("total_entradas").innerText = data.entradas;
|
document.getElementById("total_offline").innerText =
|
||||||
document.getElementById("total_saidas").innerText = data.saidas;
|
data.offline;
|
||||||
|
|
||||||
|
document.getElementById("total_entradas").innerText =
|
||||||
|
data.entradas;
|
||||||
|
|
||||||
|
document.getElementById("total_saidas").innerText =
|
||||||
|
data.saidas;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
||||||
console.error("Erro AJAX:", err);
|
console.error("Erro AJAX:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(refreshTable, 2000);
|
setInterval(refreshTable, 2000);
|
||||||
refreshTable();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
refreshTable();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
266
devices.php
Normal file
266
devices.php
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . "/api/protecao.php";
|
||||||
|
require __DIR__ . "/api/db.php";
|
||||||
|
require_once __DIR__ . "/vendor/autoload.php";
|
||||||
|
|
||||||
|
use PhpMqtt\Client\MqttClient;
|
||||||
|
use PhpMqtt\Client\ConnectionSettings;
|
||||||
|
|
||||||
|
$mqttCfg = require __DIR__ . "/config.php";
|
||||||
|
|
||||||
|
function mqtt_publish(array $cfg, string $topic, array $payload): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$settings = (new ConnectionSettings())
|
||||||
|
->setUsername($cfg['username'])
|
||||||
|
->setPassword($cfg['password'])
|
||||||
|
->setUseTls(true)
|
||||||
|
->setTlsCertificateAuthorityFile('/etc/mosquitto/certs/chain.pem')
|
||||||
|
->setConnectTimeout(3);
|
||||||
|
|
||||||
|
$client = new MqttClient(
|
||||||
|
$cfg['host'], $cfg['port'],
|
||||||
|
'web-devices-' . getmypid()
|
||||||
|
);
|
||||||
|
|
||||||
|
$client->connect($settings, true);
|
||||||
|
$client->publish($topic, json_encode($payload), 1);
|
||||||
|
$client->disconnect();
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log("MQTT publish error: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// APROVAR — desbloqueia e manda REJOIN via MQTT
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
if (isset($_GET['approve'])) {
|
||||||
|
|
||||||
|
$uid = $_GET['approve'];
|
||||||
|
|
||||||
|
$pdo->prepare("
|
||||||
|
UPDATE contadores
|
||||||
|
SET blocked=0
|
||||||
|
WHERE uid=?
|
||||||
|
")->execute([$uid]);
|
||||||
|
|
||||||
|
// REJOIN para o ESP fazer auth imediatamente
|
||||||
|
mqtt_publish($mqttCfg, "esp/$uid/cmd", ['cmd' => 'REJOIN']);
|
||||||
|
|
||||||
|
header("Location: devices.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// REJEITAR / REMOVER
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
if (isset($_GET['reject'])) {
|
||||||
|
|
||||||
|
$uid = $_GET['reject'];
|
||||||
|
|
||||||
|
$pdo->prepare("
|
||||||
|
DELETE FROM contadores
|
||||||
|
WHERE uid=?
|
||||||
|
")->execute([$uid]);
|
||||||
|
|
||||||
|
header("Location: devices.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: devices.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// DESPROMOVER — volta a pendente
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
if (isset($_GET['demote'])) {
|
||||||
|
|
||||||
|
$uid = $_GET['demote'];
|
||||||
|
|
||||||
|
$pdo->prepare("
|
||||||
|
UPDATE contadores
|
||||||
|
SET blocked=1, auth_ok=0
|
||||||
|
WHERE uid=?
|
||||||
|
")->execute([$uid]);
|
||||||
|
|
||||||
|
header("Location: devices.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// QUERY
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
$pending = $pdo->query("
|
||||||
|
SELECT uid, note, last_seen, blocked, auth_ok,
|
||||||
|
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
||||||
|
FROM contadores
|
||||||
|
WHERE blocked=1 AND auth_ok=0
|
||||||
|
ORDER BY last_seen DESC
|
||||||
|
")->fetchAll();
|
||||||
|
|
||||||
|
$approved = $pdo->query("
|
||||||
|
SELECT uid, note, last_seen, online, blocked, auth_ok, current_broker,
|
||||||
|
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
|
||||||
|
FROM contadores
|
||||||
|
WHERE blocked=0 OR auth_ok=1
|
||||||
|
ORDER BY online DESC, last_seen DESC
|
||||||
|
")->fetchAll();
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="pt" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Dispositivos</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||||
|
<style>
|
||||||
|
body { background:#0b0b0b; color:#e0e0e0; font-family:Arial,sans-serif; }
|
||||||
|
.card { background:#151515; border:1px solid #333; }
|
||||||
|
.header-title { color:#00ffcc; text-shadow:0 0 8px rgba(0,255,180,0.4); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
|
||||||
|
<h1 class="mb-4 header-title">🔌 Gestão de Dispositivos</h1>
|
||||||
|
|
||||||
|
<!-- PENDENTES -->
|
||||||
|
<h4 class="text-warning mb-3">⏳ Pendentes de Aprovação (<?= count($pending) ?>)</h4>
|
||||||
|
|
||||||
|
<?php if (empty($pending)): ?>
|
||||||
|
<div class="alert alert-secondary mb-4">Nenhum dispositivo pendente.</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="card shadow-sm mb-5">
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-dark table-hover align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>UID</th>
|
||||||
|
<th>NOTA</th>
|
||||||
|
<th>ÚLTIMO HELLO</th>
|
||||||
|
<th>AÇÕES</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($pending as $r): ?>
|
||||||
|
<?php
|
||||||
|
$ago = (int)$r['ago'];
|
||||||
|
$lastSeen = $ago < 60 ? "{$ago}s" : round($ago/60) . "m";
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold text-warning"><?= htmlspecialchars($r['uid']) ?></td>
|
||||||
|
<td><?= htmlspecialchars($r['note'] ?? '') ?></td>
|
||||||
|
<td><?= $lastSeen ?></td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="?approve=<?= urlencode($r['uid']) ?>"
|
||||||
|
class="btn btn-sm btn-success"
|
||||||
|
onclick="return confirm('Aprovar <?= htmlspecialchars($r['uid']) ?>?')">
|
||||||
|
✅ Aprovar
|
||||||
|
</a>
|
||||||
|
<a href="?reject=<?= urlencode($r['uid']) ?>"
|
||||||
|
class="btn btn-sm btn-danger"
|
||||||
|
onclick="return confirm('Rejeitar e apagar <?= htmlspecialchars($r['uid']) ?>?')">
|
||||||
|
❌ Rejeitar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- APROVADOS -->
|
||||||
|
<h4 class="text-success mb-3">✅ Dispositivos Aprovados (<?= count($approved) ?>)</h4>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-dark table-striped table-hover align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>UID</th>
|
||||||
|
<th>NOTA</th>
|
||||||
|
<th>ESTADO</th>
|
||||||
|
<th>BROKER</th>
|
||||||
|
<th>ÚLTIMO</th>
|
||||||
|
<th>AÇÕES</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($approved as $r): ?>
|
||||||
|
<?php
|
||||||
|
$ago = (int)$r['ago'];
|
||||||
|
$lastSeen = $ago < 60 ? "{$ago}s" : round($ago/60) . "m";
|
||||||
|
$online = (int)$r['online'] === 1;
|
||||||
|
$blocked = (int)$r['blocked'] === 1;
|
||||||
|
|
||||||
|
if ($blocked) {
|
||||||
|
$state = '<span class="badge bg-danger">BLOQUEADO</span>';
|
||||||
|
} elseif ($online) {
|
||||||
|
$state = '<span class="badge bg-success">ONLINE</span>';
|
||||||
|
} else {
|
||||||
|
$state = '<span class="badge bg-secondary">OFFLINE</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$broker = ($r['current_broker'] ?? '') === 'recovery'
|
||||||
|
? '<span class="badge bg-warning text-dark">RECOVERY</span>'
|
||||||
|
: '<span class="badge bg-primary">PRIMARY</span>';
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold"><?= htmlspecialchars($r['uid']) ?></td>
|
||||||
|
<td><?= htmlspecialchars($r['note'] ?? '') ?></td>
|
||||||
|
<td><?= $state ?></td>
|
||||||
|
<td><?= $broker ?></td>
|
||||||
|
<td><?= $lastSeen ?></td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="?toggle=<?= urlencode($r['uid']) ?>"
|
||||||
|
class="btn btn-sm <?= $blocked ? 'btn-success' : 'btn-danger' ?>">
|
||||||
|
<?= $blocked ? 'Desbloquear' : 'Bloquear' ?>
|
||||||
|
</a>
|
||||||
|
<a href="?demote=<?= urlencode($r['uid']) ?>"
|
||||||
|
class="btn btn-sm btn-warning"
|
||||||
|
onclick="return confirm('Despromover para pendente?')">
|
||||||
|
↩ Pendente
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="api/menu.php" class="btn btn-outline-secondary">← Menu</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
save_note.php
Normal file
16
save_note.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . "/api/protecao.php";
|
||||||
|
require __DIR__ . "/api/db.php";
|
||||||
|
|
||||||
|
$uid = $_POST['uid'] ?? '';
|
||||||
|
$note = $_POST['note'] ?? '';
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE contadores
|
||||||
|
SET note = ?
|
||||||
|
WHERE uid = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([$note, $uid]);
|
||||||
|
|
||||||
|
header("Location: console.php");
|
||||||
Loading…
Reference in New Issue
Block a user