www/devices.php
2026-06-27 21:44:44 +00:00

299 lines
10 KiB
PHP

<?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,
COALESCE(brightness, 128) AS brightness,
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>BRILHO</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>';
?>
<?php $bri = (int)($r['brightness'] ?? 128); ?>
<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 align-items-center gap-2">
<input type="range" class="form-range" min="0" max="255"
value="<?= $bri ?>"
id="bri_<?= htmlspecialchars($r['uid']) ?>"
style="width:110px"
oninput="setBri('<?= htmlspecialchars($r['uid']) ?>',this.value)"
<?= $online ? '' : 'disabled' ?>>
<span id="brival_<?= htmlspecialchars($r['uid']) ?>"
class="text-muted" style="min-width:28px;font-size:.85em">
<?= $bri ?>
</span>
</div>
</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>
<script>
const briTimers = {};
function setBri(uid, val) {
document.getElementById('brival_' + uid).textContent = val;
clearTimeout(briTimers[uid]);
briTimers[uid] = setTimeout(() => {
const body = new URLSearchParams({ uid, value: val });
fetch('/api/set_brightness.php', { method: 'POST', body })
.then(r => r.json())
.then(d => { if (!d.ok) console.warn('brightness err', d); })
.catch(e => console.error(e));
}, 350);
}
</script>
</body>
</html>