661 lines
16 KiB
PHP
661 lines
16 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . "/api/protecao.php";
|
|
require __DIR__ . "/api/db.php";
|
|
|
|
// =====================================================
|
|
// GUARDAR NOTA
|
|
// =====================================================
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_note'])) {
|
|
|
|
$uid = $_POST['uid'] ?? '';
|
|
$note = trim($_POST['note'] ?? '');
|
|
|
|
$stmt = $pdo->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++;
|
|
}
|
|
}
|
|
?>
|
|
<!doctype html>
|
|
<html lang="pt" data-bs-theme="dark">
|
|
|
|
<head>
|
|
|
|
<meta charset="utf-8">
|
|
|
|
<title>MQTT Swarm Console</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;
|
|
}
|
|
|
|
.table {
|
|
color: #ddd;
|
|
}
|
|
|
|
.table-striped > tbody > tr:nth-of-type(odd) {
|
|
background-color: #171717;
|
|
}
|
|
|
|
.table-striped > tbody > tr:nth-of-type(even) {
|
|
background-color: #101010;
|
|
}
|
|
|
|
.table-hover tbody tr:hover {
|
|
background-color: #202020;
|
|
}
|
|
|
|
.note-input {
|
|
min-width: 220px;
|
|
background: #111;
|
|
border: 1px solid #333;
|
|
color: #fff;
|
|
}
|
|
|
|
.note-input:focus {
|
|
background: #181818;
|
|
color: #fff;
|
|
border-color: #58a6ff;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.pulse-cell {
|
|
animation: pulse 1s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
.header-title {
|
|
color: #00ffcc;
|
|
text-shadow: 0 0 8px rgba(0,255,180,0.4);
|
|
}
|
|
|
|
.small-info {
|
|
font-size: 12px;
|
|
color: #999;
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="container-fluid py-4">
|
|
|
|
<h1 class="mb-4 header-title">
|
|
🌋 MQTT SWARM CONSOLE
|
|
</h1>
|
|
|
|
<!-- RESUMO -->
|
|
<div class="row mb-4 text-center">
|
|
|
|
<div class="col-md-3">
|
|
<div class="p-3 card rounded">
|
|
<h4 id="total_online"><?= $onlineCount ?></h4>
|
|
<div class="text-success fw-bold">
|
|
🟢 ONLINE
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="p-3 card rounded">
|
|
<h4 id="total_offline"><?= $offlineCount ?></h4>
|
|
<div class="text-danger fw-bold">
|
|
🔴 OFFLINE
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="p-3 card rounded">
|
|
<h4 id="total_entradas"><?= $sumEntradas ?></h4>
|
|
<div class="fw-bold">
|
|
💰 ENTRADAS
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="p-3 card rounded">
|
|
<h4 id="total_saidas"><?= $sumSaidas ?></h4>
|
|
<div class="fw-bold">
|
|
💸 SAÍDAS
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- TABELA -->
|
|
<div class="card shadow-sm">
|
|
|
|
<div class="card-body">
|
|
|
|
<table class="table table-striped table-hover align-middle">
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th>UID</th>
|
|
<th>NOTA</th>
|
|
<th>ENTRADAS</th>
|
|
<th>SAÍDAS</th>
|
|
<th>ESTADO</th>
|
|
<th>BROKER</th>
|
|
<th>RSSI</th>
|
|
<th>HEAP</th>
|
|
<th>UPTIME</th>
|
|
<th>ÚLTIMO</th>
|
|
<th>AÇÕES</th>
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody id="tabela_corpo">
|
|
|
|
<?php foreach ($rows as $r): ?>
|
|
|
|
<?php
|
|
|
|
$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>';
|
|
}
|
|
|
|
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"
|
|
: "";
|
|
|
|
?>
|
|
|
|
<tr class="<?= $pulseClass ?>">
|
|
|
|
<td class="fw-bold">
|
|
<?= htmlspecialchars($r['uid']) ?>
|
|
</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>
|
|
|
|
<?php endforeach; ?>
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
|
|
function esc(s) {
|
|
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<')
|
|
.replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
async function refreshTable() {
|
|
|
|
// Não actualizar se o utilizador estiver a editar uma nota
|
|
if (document.activeElement && document.activeElement.classList.contains('note-input')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
|
|
const res =
|
|
await fetch(
|
|
'/api/contadores_json.php?x='
|
|
+ Date.now()
|
|
);
|
|
|
|
const data = await res.json();
|
|
|
|
let html = "";
|
|
|
|
for (const r of data.rows) {
|
|
|
|
let state = "";
|
|
|
|
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>';
|
|
}
|
|
|
|
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 += `
|
|
<tr class="${esc(pulseClass)}">
|
|
|
|
<td class="fw-bold">
|
|
${esc(r.uid)}
|
|
</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>
|
|
`;
|
|
}
|
|
|
|
document.getElementById("tabela_corpo").innerHTML = html;
|
|
|
|
document.getElementById("total_online").innerText =
|
|
data.online;
|
|
|
|
document.getElementById("total_offline").innerText =
|
|
data.offline;
|
|
|
|
document.getElementById("total_entradas").innerText =
|
|
data.entradas;
|
|
|
|
document.getElementById("total_saidas").innerText =
|
|
data.saidas;
|
|
|
|
} catch (err) {
|
|
|
|
console.error("Erro AJAX:", err);
|
|
}
|
|
}
|
|
|
|
setInterval(refreshTable, 2000);
|
|
|
|
refreshTable();
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|