fix: auto-approve HMAC + DELETE_CERT no delete + cross-DB cleanup
This commit is contained in:
parent
095ce47e01
commit
fdad258386
@ -59,7 +59,7 @@ $db = new PDO(
|
||||
/* =========================
|
||||
* MQTT CONFIG
|
||||
* ========================= */
|
||||
$mqttHost = 'mqtt.xupas.mywire.org';
|
||||
$mqttHost = '192.168.10.106'; // CT106: broker com a frota TODA (bridge VPS esp/+/status both)
|
||||
$mqttUser = 'xupa';
|
||||
$mqttPass = 'xupa';
|
||||
|
||||
@ -73,6 +73,7 @@ $tlsSettings = (new ConnectionSettings())
|
||||
->setPassword($mqttPass)
|
||||
->setUseTls(true)
|
||||
->setTlsSelfSignedAllowed(true)
|
||||
->setTlsVerifyPeerName(false)
|
||||
->setKeepAliveInterval(30)
|
||||
->setConnectTimeout(5)
|
||||
->setSocketTimeout(1)
|
||||
@ -107,6 +108,8 @@ $subscribeTls = function () use ($mqttTls, $db, $deviceSecret) {
|
||||
$rssi = isset($json['rssi']) ? (int)$json['rssi'] : null;
|
||||
$broker = isset($json['broker']) ? (string)$json['broker'] : null;
|
||||
$brightness = isset($json['brightness']) ? (int)$json['brightness'] : null;
|
||||
$fw = isset($json['fw']) ? (string)$json['fw'] : null;
|
||||
$chip = isset($json['chip']) ? (string)$json['chip'] : null;
|
||||
$hmac = isset($json['hmac']) ? strtoupper((string)$json['hmac']) : null;
|
||||
|
||||
if ($hmac === null || $uptime === null) {
|
||||
@ -114,7 +117,11 @@ $subscribeTls = function () use ($mqttTls, $db, $deviceSecret) {
|
||||
return;
|
||||
}
|
||||
|
||||
$expected = strtoupper(hash_hmac('sha256', $uid . '|' . $uptime, $deviceSecret));
|
||||
$_m = $uid . '|' . $uptime;
|
||||
$_eg = strtoupper(hash_hmac('sha256', $_m, $deviceSecret)); // global
|
||||
$_dsec = hash_hmac('sha256', $uid, 'af2e8f1b5411d7df7c9b3dba45fa4ca4517329d28d2550d29be898d34b015952');
|
||||
$_ed = strtoupper(hash_hmac('sha256', $_m, $_dsec)); // Fase C por-device
|
||||
$expected = hash_equals($_ed, (string)$hmac) ? $_ed : $_eg; // aceita global OU per-device
|
||||
|
||||
if (!hash_equals($expected, $hmac)) {
|
||||
logmsg("STATUS HMAC INVÁLIDO $uid — ignorado", LOG_ERROR);
|
||||
@ -124,10 +131,10 @@ $subscribeTls = function () use ($mqttTls, $db, $deviceSecret) {
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO contadores
|
||||
(uid, entradas, saidas, entradas_total, saidas_total, last_seen, online,
|
||||
uptime, heap, rssi, current_broker, brightness)
|
||||
uptime, heap, rssi, current_broker, brightness, fw_ver, chip)
|
||||
VALUES
|
||||
(:uid, 0, 0, 0, 0, NOW(), 1,
|
||||
:uptime, :heap, :rssi, :broker, :brightness)
|
||||
:uptime, :heap, :rssi, :broker, :brightness, :fw, :chip)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_seen=NOW(),
|
||||
online=1,
|
||||
@ -135,7 +142,10 @@ $subscribeTls = function () use ($mqttTls, $db, $deviceSecret) {
|
||||
heap=COALESCE(:heap2, heap),
|
||||
rssi=COALESCE(:rssi2, rssi),
|
||||
current_broker=COALESCE(:broker2, current_broker),
|
||||
brightness=COALESCE(:brightness2, brightness)
|
||||
brightness=COALESCE(:brightness2, brightness),
|
||||
fw_ver=COALESCE(:fw2, fw_ver),
|
||||
chip=COALESCE(:chip2, chip),
|
||||
auth_ok=IF(blocked=0,1,auth_ok)
|
||||
");
|
||||
$stmt->execute([
|
||||
'uid' => $uid,
|
||||
@ -149,12 +159,23 @@ $subscribeTls = function () use ($mqttTls, $db, $deviceSecret) {
|
||||
'rssi2' => $rssi,
|
||||
'broker2' => $broker,
|
||||
'brightness2' => $brightness,
|
||||
'fw' => $fw,
|
||||
'fw2' => $fw,
|
||||
'chip' => $chip,
|
||||
'chip2' => $chip,
|
||||
]);
|
||||
|
||||
$mqttTls->publish("esp/$uid/cmd",
|
||||
json_encode(['cmd' => 'HEARTBEAT_OK']), 1, false);
|
||||
$chk = $db->prepare("SELECT blocked FROM contadores WHERE uid=:uid");
|
||||
$chk->execute(['uid' => $uid]);
|
||||
$row = $chk->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
logmsg("STATUS OK $uid uptime=$uptime heap=$heap rssi=$rssi broker=$broker → HEARTBEAT_OK");
|
||||
if (!$row || (int)($row['blocked'] ?? 0) === 0) {
|
||||
$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");
|
||||
} else {
|
||||
logmsg("STATUS BLOQUEADO $uid — sem HEARTBEAT_OK", LOG_ERROR);
|
||||
}
|
||||
|
||||
}, 0);
|
||||
|
||||
|
||||
25
api/ota_trigger.php
Normal file
25
api/ota_trigger.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
header('Content-Type: application/json');
|
||||
require __DIR__ . '/db.php';
|
||||
require __DIR__ . '/mqtt.php';
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$uid = $data['uid'] ?? null;
|
||||
$url = $data['url'] ?? null;
|
||||
$cmd = $data['cmd'] ?? 'OTA_CHECK';
|
||||
|
||||
if (!$url) { http_response_code(400); echo json_encode(['error'=>'url_required']); exit; }
|
||||
if (!in_array($cmd, ['OTA_CHECK','OTA_URL'], true)) { http_response_code(400); echo json_encode(['error'=>'cmd_invalid']); exit; }
|
||||
|
||||
$payload = json_encode(['cmd'=>$cmd,'url'=>$url], JSON_UNESCAPED_SLASHES);
|
||||
|
||||
if ($uid === 'all') {
|
||||
$rows = $pdo->query("SELECT uid FROM contadores WHERE blocked=0 AND online=1")->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($rows as $r) mqtt_publish("esp/{$r['uid']}/cmd", $payload);
|
||||
echo json_encode(['ok'=>1,'sent'=>count($rows)]);
|
||||
} else {
|
||||
if (!$uid) { http_response_code(400); echo json_encode(['error'=>'uid_required']); exit; }
|
||||
mqtt_publish("esp/$uid/cmd", $payload);
|
||||
echo json_encode(['ok'=>1]);
|
||||
}
|
||||
137
esp_devices.php
Normal file
137
esp_devices.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (empty($_SESSION['loggedin'])) { header('Location: index.php'); exit; }
|
||||
|
||||
define('PMX_API', 'http://192.168.10.106:8765/?key=esp_api_xupas_2025');
|
||||
|
||||
function pmx_get(): array {
|
||||
$ch = curl_init(PMX_API.'&action=list');
|
||||
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>8]);
|
||||
$r = json_decode(curl_exec($ch),true); curl_close($ch);
|
||||
return $r['devices'] ?? [];
|
||||
}
|
||||
|
||||
function pmx_post(array $data): array {
|
||||
$ch = curl_init(PMX_API);
|
||||
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>http_build_query($data),CURLOPT_TIMEOUT=>8]);
|
||||
$r = json_decode(curl_exec($ch),true); curl_close($ch);
|
||||
return $r ?? [];
|
||||
}
|
||||
|
||||
$notice = ''; $error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$uid = preg_replace('/[^0-9A-Fa-f]/', '', $_POST['uid'] ?? '');
|
||||
if (!$uid) { $error = 'UID inválido.'; }
|
||||
else {
|
||||
$res = pmx_post($_POST);
|
||||
if (!empty($res['ok'])) $notice = "[{$uid}] {$_POST['action']} OK";
|
||||
else $error = $res['error'] ?? 'Erro';
|
||||
}
|
||||
$qs = $notice ? '?ok='.urlencode($notice) : ($error ? '?err='.urlencode($error) : '');
|
||||
header("Location: esp_devices.php$qs"); exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['ok'])) $notice = htmlspecialchars($_GET['ok']);
|
||||
if (isset($_GET['err'])) $error = htmlspecialchars($_GET['err']);
|
||||
|
||||
$devices = pmx_get();
|
||||
function h($s) { return htmlspecialchars((string)$s, ENT_QUOTES); }
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>ESP Devices — Proxmox</title>
|
||||
<style>
|
||||
* { box-sizing:border-box; margin:0; padding:0; }
|
||||
body { background:#0d1117; color:#e6edf3; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; font-size:14px; }
|
||||
.header { background:#161b22; border-bottom:1px solid #21262d; padding:14px 24px; display:flex; align-items:center; gap:16px; }
|
||||
.header a { color:#58a6ff; text-decoration:none; font-size:13px; }
|
||||
.header h1 { font-size:16px; font-weight:600; flex:1; }
|
||||
.wrap { max-width:1200px; margin:0 auto; padding:24px; }
|
||||
.flash-ok { background:#0d4f2a; color:#3fb950; padding:10px 16px; border-radius:6px; margin-bottom:16px; }
|
||||
.flash-err { background:#4f1010; color:#da3633; padding:10px 16px; border-radius:6px; margin-bottom:16px; }
|
||||
table { width:100%; border-collapse:collapse; font-size:13px; }
|
||||
th { background:#1e2733; padding:10px 12px; text-align:left; color:#aaa; font-weight:normal; }
|
||||
td { padding:9px 12px; border-bottom:1px solid #1e2733; vertical-align:middle; }
|
||||
tr:hover td { background:#111822; }
|
||||
.badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:11px; }
|
||||
.badge-ok { background:#0d4f2a; color:#3fb950; }
|
||||
.badge-block { background:#4f1010; color:#da3633; }
|
||||
.btn { display:inline-block; padding:4px 9px; border-radius:4px; border:none; cursor:pointer; font-size:12px; }
|
||||
.btn:hover { filter:brightness(1.3); }
|
||||
.btn-green { background:#1a4a1a; color:#3fb950; }
|
||||
.btn-red { background:#4f1010; color:#da3633; }
|
||||
.btn-grey { background:#2a2a2a; color:#ccc; }
|
||||
.btn-blue { background:#0d2a4f; color:#58a6ff; }
|
||||
.btn-orange { background:#4f3210; color:#d29922; }
|
||||
.btn-purple { background:#2d1a4f; color:#bc8cff; }
|
||||
.uid { font-family:monospace; }
|
||||
.ago { color:#666; font-size:11px; }
|
||||
form { display:inline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<a href="index.php">← Painel</a>
|
||||
<h1>ESP Devices <span style="color:#bc8cff;font-size:13px;font-weight:400">— Proxmox broker</span></h1>
|
||||
<span style="color:#666;font-size:12px"><?= count($devices) ?> dispositivos</span>
|
||||
</div>
|
||||
<div class="wrap">
|
||||
|
||||
<?php if ($notice): ?><div class="flash-ok"><?= $notice ?></div><?php endif; ?>
|
||||
<?php if ($error): ?><div class="flash-err"><?= $error ?></div><?php endif; ?>
|
||||
|
||||
<?php if (!$devices): ?>
|
||||
<p style="color:#666;margin-top:20px">Sem dispositivos ou erro a ligar ao broker Proxmox.</p>
|
||||
<?php else: ?>
|
||||
<table>
|
||||
<thead><tr><th>UID</th><th>Broker atual</th><th>Estado</th><th>Último contacto</th><th>Heap</th><th>RSSI</th><th>Ações</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($devices as $d):
|
||||
$uid = $d['uid'];
|
||||
$blocked= (bool)$d['blocked'];
|
||||
$broker = $d['current_broker'] ?? '—';
|
||||
$last = $d['last_seen'] ?? '-';
|
||||
$ago = '-';
|
||||
if ($last && $last !== '-') {
|
||||
$diff = time() - strtotime($last);
|
||||
if ($diff < 60) $ago = $diff.'s atrás';
|
||||
else if ($diff < 3600) $ago = round($diff/60).'min atrás';
|
||||
else $ago = round($diff/3600).'h atrás';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><span class="uid"><?= h($uid) ?></span></td>
|
||||
<td style="font-size:12px;color:var(--muted,#8b949e)"><?= h($broker) ?></td>
|
||||
<td><?php if ($blocked): ?>
|
||||
<span class="badge badge-block">bloqueado</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-ok">aprovado</span>
|
||||
<?php endif; ?></td>
|
||||
<td><?= h($last) ?><br><span class="ago"><?= $ago ?></span></td>
|
||||
<td style="color:#666;font-size:12px"><?= $d['heap'] ? number_format((int)$d['heap']) : '—' ?></td>
|
||||
<td style="color:#666;font-size:12px"><?= $d['rssi'] ? $d['rssi'].' dBm' : '—' ?></td>
|
||||
<td style="white-space:nowrap">
|
||||
<?php if ($blocked): ?>
|
||||
<form method="post"><input type="hidden" name="uid" value="<?= h($uid) ?>"><input type="hidden" name="action" value="approve"><button class="btn btn-green">Aprovar</button></form>
|
||||
<?php else: ?>
|
||||
<form method="post"><input type="hidden" name="uid" value="<?= h($uid) ?>"><input type="hidden" name="action" value="block"><button class="btn btn-red">Bloquear</button></form>
|
||||
<?php endif; ?>
|
||||
<?php foreach ([['REBOOT','btn-orange','Reboot'],['STATUS','btn-grey','Status'],['SWITCH_PRIMARY','btn-blue','→Main'],['REJOIN','btn-grey','Rejoin']] as [$c,$cl,$lb]): ?>
|
||||
<form method="post"><input type="hidden" name="uid" value="<?= h($uid) ?>"><input type="hidden" name="action" value="cmd"><input type="hidden" name="cmd" value="<?= $c ?>"><button class="btn <?= $cl ?>"><?= $lb ?></button></form>
|
||||
<?php endforeach; ?>
|
||||
<form method="post"><input type="hidden" name="uid" value="<?= h($uid) ?>"><input type="hidden" name="action" value="switch_pair"><input type="hidden" name="pair" value="0"><button class="btn btn-blue">→xpto</button></form>
|
||||
<form method="post"><input type="hidden" name="uid" value="<?= h($uid) ?>"><input type="hidden" name="action" value="switch_pair"><input type="hidden" name="pair" value="1"><button class="btn btn-purple">→xupas</button></form>
|
||||
<form method="post" onsubmit="return confirm('Remover?')"><input type="hidden" name="uid" value="<?= h($uid) ?>"><input type="hidden" name="action" value="delete"><button class="btn btn-red">✕</button></form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
146
esp_proxmox.php
Normal file
146
esp_proxmox.php
Normal file
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
require_once __DIR__.'/inc/common.php';
|
||||
|
||||
define('PMX_API', 'http://192.168.10.106:8765/?key=esp_api_xupas_2025');
|
||||
|
||||
// Lista lida do MySQL local (cagalhao) — atualizado ao vivo pelo
|
||||
// mqtt_listener.php deste container. (A API do 106 servia uma SQLite
|
||||
// morta do antigo listener python; fica só para comandos MQTT.)
|
||||
function pmx_get(): array {
|
||||
try {
|
||||
$pdo = new PDO('mysql:host=localhost;dbname=cagalhao;charset=utf8',
|
||||
'master', 'master',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
return $pdo->query("
|
||||
SELECT uid, online, blocked, current_broker, last_seen,
|
||||
brightness, heap, rssi, fw_ver, fw_ver,
|
||||
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago_s
|
||||
FROM contadores
|
||||
ORDER BY uid ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function pmx_post(array $data): array {
|
||||
$ch = curl_init(PMX_API);
|
||||
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>http_build_query($data),CURLOPT_TIMEOUT=>8]);
|
||||
$r = json_decode(curl_exec($ch),true); curl_close($ch);
|
||||
return $r ?? [];
|
||||
}
|
||||
|
||||
$msg = ''; $msgOk = true;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$uid = preg_replace('/[^0-9A-Fa-f]/', '', $_POST['uid'] ?? '');
|
||||
if (!$uid) { $msg = 'UID invalido.'; $msgOk = false; }
|
||||
else {
|
||||
$res = pmx_post($_POST);
|
||||
if (!empty($res['ok'])) { $msg = "[$uid] {$_POST['action']} OK"; }
|
||||
else { $msg = $res['error'] ?? 'Erro'; $msgOk = false; }
|
||||
}
|
||||
$qs = $msg ? ($msgOk ? '?ok='.urlencode($msg) : '?err='.urlencode($msg)) : '';
|
||||
header("Location: /esp_proxmox.php$qs"); exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['ok'])) { $msg = htmlspecialchars($_GET['ok']); $msgOk = true; }
|
||||
if (isset($_GET['err'])) { $msg = htmlspecialchars($_GET['err']); $msgOk = false; }
|
||||
|
||||
$devices = pmx_get();
|
||||
$title = "ESP Devices — Proxmox";
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="breadcrumb"><a href="/painel.php">Painel</a><span class="sep">›</span>ESP Devices<span class="sep">›</span>Proxmox</div>
|
||||
<h1>ESP Devices <span style="color:var(--muted);font-weight:400;font-size:16px">Proxmox broker</span></h1>
|
||||
</div>
|
||||
<a href="/esp_vps.php" class="btn btn-secondary">← Ver VPS</a>
|
||||
</div>
|
||||
|
||||
<?php if ($msg): ?>
|
||||
<div class="alert <?= $msgOk ? 'alert-success' : 'alert-error' ?>"><?= $msgOk ? '✓' : '⚠' ?> <?= $msg ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$devices): ?>
|
||||
<div class="empty-state"><p>Sem dispositivos ou erro a ligar ao broker Proxmox.</p></div>
|
||||
<?php else: ?>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Dispositivos <span style="color:var(--muted);font-weight:400;font-size:13px">(<?= count($devices) ?>)</span></h2>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>UID</th><th>Versão</th><th>Versão</th><th>Ultimo contacto</th><th>Online</th><th>Estado</th><th>Brilho</th><th>Ações</th><th>Broker</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($devices as $d):
|
||||
$uid=$d['uid']; $blocked=(bool)$d['blocked']; $online=(bool)$d['online'];
|
||||
$broker=$d['current_broker']??'--';
|
||||
$last=$d['last_seen']??'-'; $bri=(int)($d['brightness']??128);
|
||||
// idade calculada no MySQL (TIMESTAMPDIFF) — imune a fusos PHP/DB
|
||||
$diff=isset($d['ago_s'])?(int)$d['ago_s']:($last&&$last!=='-'?time()-strtotime($last):null);
|
||||
// bolinha verde no botão do broker onde está ligado
|
||||
$isPmx=$online && strpos((string)$broker,'remote')===0;
|
||||
$isVps=$online && !$isPmx && $broker!=='--';
|
||||
$dot='<span style="display:inline-block;width:7px;height:7px;border-radius:50%;background:#3fb950;box-shadow:0 0 5px #3fb950;margin-right:4px;vertical-align:middle"></span>';
|
||||
$ago=$diff===null?'-':($diff<60?$diff.'s':($diff<3600?round($diff/60).'min':round($diff/3600).'h')).' atras';
|
||||
?>
|
||||
<tr>
|
||||
<td style="font-family:monospace;font-size:12px"><?= h($uid) ?></td><td style="font-size:12px;color:#8b949e"><?= ($d['fw_ver']??'')!=='' ? h($d['fw_ver']) : '—' ?></td>
|
||||
<td style="font-size:12px;color:var(--muted)"><?= h($d['fw_ver'] ?? '—') ?></td>
|
||||
<td style="color:var(--muted);font-size:12px"><?= h($last) ?><br><span style="font-size:11px"><?= $ago ?></span></td>
|
||||
<td><?php if($online):?><span class="badge" style="background:#0d2818;color:#3fb950">online</span><?php else:?><span class="badge" style="background:#1e2733;color:var(--muted)">offline</span><?php endif;?></td>
|
||||
<td><?php if($blocked):?><span class="badge" style="background:#2d1a1a;color:#f85149">bloqueado</span><?php else:?><span class="badge" style="background:#0d2818;color:#3fb950">aprovado</span><?php endif;?></td>
|
||||
<td><div style="display:flex;align-items:center;gap:6px">
|
||||
<input type="range" min="0" max="255" value="<?= $bri ?>" id="bri_<?= h($uid) ?>" style="width:90px;accent-color:var(--accent)" oninput="setBri('<?= h($uid) ?>',this.value)" <?= $online?'':'disabled'?>>
|
||||
<span id="brival_<?= h($uid) ?>" style="font-size:11px;color:var(--muted);min-width:24px"><?= $bri ?></span>
|
||||
</div></td>
|
||||
<td style="white-space:nowrap">
|
||||
<?php if($blocked):?>
|
||||
<button class="btn btn-secondary btn-sm" data-uid="<?= h($uid) ?>" data-act="approve">Aprovar</button>
|
||||
<?php else:?>
|
||||
<button class="btn btn-danger btn-sm" data-uid="<?= h($uid) ?>" data-act="block">Bloquear</button>
|
||||
<?php endif;?>
|
||||
<button class="btn btn-secondary btn-sm" data-uid="<?= h($uid) ?>" data-act="cmd" data-cmd="REBOOT">Reboot</button>
|
||||
<button class="btn btn-danger btn-sm" data-uid="<?= h($uid) ?>" data-act="delete">✕</button>
|
||||
</td>
|
||||
<td style="white-space:nowrap">
|
||||
<button class="btn btn-secondary btn-sm" data-uid="<?= h($uid) ?>" data-act="switch_pair" data-pair="0"><?= $isVps?$dot:'' ?>→ VPS</button>
|
||||
<button class="btn btn-secondary btn-sm" data-uid="<?= h($uid) ?>" data-act="switch_pair" data-pair="1"><?= $isPmx?$dot:'' ?>→ Proxmox</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach;?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif;?>
|
||||
|
||||
<script>
|
||||
const _bt={};
|
||||
function act(uid, action, extra){
|
||||
const fd=new FormData(); fd.append('uid',uid); fd.append('action',action);
|
||||
if(extra) Object.keys(extra).forEach(k=>fd.append(k,extra[k]));
|
||||
fetch('/esp_proxmox.php',{method:'POST',body:fd}).then(()=>location.reload());
|
||||
}
|
||||
document.addEventListener('click',function(e){
|
||||
const b=e.target.closest('button[data-act]'); if(!b) return;
|
||||
if(b.dataset.act==='delete' && !confirm('Remover '+b.dataset.uid+'?')) return;
|
||||
const extra={};
|
||||
if(b.dataset.pair!==undefined) extra.pair=b.dataset.pair;
|
||||
if(b.dataset.cmd!==undefined) extra.cmd=b.dataset.cmd;
|
||||
act(b.dataset.uid,b.dataset.act,extra);
|
||||
});
|
||||
function setBri(uid,val){
|
||||
document.getElementById('brival_'+uid).textContent=val;
|
||||
clearTimeout(_bt[uid]);
|
||||
_bt[uid]=setTimeout(()=>{
|
||||
const fd=new FormData();
|
||||
fd.append('action','brightness');fd.append('uid',uid);fd.append('value',val);
|
||||
fetch('/esp_proxmox.php',{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:fd});
|
||||
},400);
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php $content = ob_get_clean(); require __DIR__.'/inc/layout.php';
|
||||
135
esp_vps.php
Normal file
135
esp_vps.php
Normal file
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
function h($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
define('VPS_API', 'https://web.xpto.ooguy.com/esp_api.php?key=esp_api_xupas_2025');
|
||||
|
||||
function vps_get(): array {
|
||||
$ch = curl_init(VPS_API.'&action=list');
|
||||
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_SSL_VERIFYPEER=>false,CURLOPT_TIMEOUT=>8]);
|
||||
$r = json_decode(curl_exec($ch),true); curl_close($ch);
|
||||
return $r['devices'] ?? [];
|
||||
}
|
||||
function vps_post(array $data): array {
|
||||
$ch = curl_init(VPS_API);
|
||||
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>http_build_query($data),CURLOPT_SSL_VERIFYPEER=>false,CURLOPT_TIMEOUT=>8]);
|
||||
$r = json_decode(curl_exec($ch),true); curl_close($ch);
|
||||
return $r ?? [];
|
||||
}
|
||||
|
||||
$msg = ''; $msgOk = true;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$uid = preg_replace('/[^0-9A-Fa-f]/', '', $_POST['uid'] ?? '');
|
||||
if (!$uid) { $msg = 'UID invalido.'; $msgOk = false; }
|
||||
else {
|
||||
$res = vps_post($_POST);
|
||||
if (!empty($res['ok'])) { $msg = "[$uid] {$_POST['action']} OK"; }
|
||||
else { $msg = $res['error'] ?? 'Erro'; $msgOk = false; }
|
||||
}
|
||||
$qs = $msg ? ($msgOk ? '?ok='.urlencode($msg) : '?err='.urlencode($msg)) : '';
|
||||
header("Location: /esp_vps.php$qs"); exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['ok'])) { $msg = htmlspecialchars($_GET['ok']); $msgOk = true; }
|
||||
if (isset($_GET['err'])) { $msg = htmlspecialchars($_GET['err']); $msgOk = false; }
|
||||
|
||||
$devices = vps_get(); usort($devices, function($a,$b){ return strcmp((string)($a['uid']??''),(string)($b['uid']??'')); });
|
||||
$title = "ESP Devices — VPS";
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="breadcrumb"><a href="/painel.php">Painel</a><span class="sep">›</span>ESP Devices<span class="sep">›</span>VPS</div>
|
||||
<h1>ESP Devices <span style="color:var(--muted);font-weight:400;font-size:16px">VPS broker</span></h1>
|
||||
</div>
|
||||
<a href="/esp_proxmox.php" class="btn btn-secondary">Ver Proxmox →</a>
|
||||
</div>
|
||||
|
||||
<?php if ($msg): ?>
|
||||
<div class="alert <?= $msgOk ? 'alert-success' : 'alert-error' ?>"><?= $msgOk ? '✓' : '⚠' ?> <?= $msg ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$devices): ?>
|
||||
<div class="empty-state"><p>Sem dispositivos ou erro a ligar a VPS.</p></div>
|
||||
<?php else: ?>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Dispositivos <span style="color:var(--muted);font-weight:400;font-size:13px">(<?= count($devices) ?>)</span></h2>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>UID</th><th>Versão</th><th>Versão</th><th>Online</th><th>Estado</th><th>Ultimo contacto</th><th>Brilho</th><th></th><th>Broker</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($devices as $d):
|
||||
$uid=$d['uid']; $online=(bool)$d['online']; $blocked=(bool)$d['blocked']; $auth_ok=(bool)$d['auth_ok'];
|
||||
$last=$d['last_seen']??'-'; $bri=(int)($d['brightness']??128);
|
||||
$diff=$last&&$last!=='-'?time()-strtotime($last):null;
|
||||
// bolinha verde no botão do broker onde está ligado
|
||||
$broker=(string)($d['current_broker']??'');
|
||||
$isPmx=$online && strpos($broker,'remote')===0;
|
||||
$isVps=$online && !$isPmx && $broker!=='';
|
||||
$dot='<span style="display:inline-block;width:7px;height:7px;border-radius:50%;background:#3fb950;box-shadow:0 0 5px #3fb950;margin-right:4px;vertical-align:middle"></span>';
|
||||
$ago=$diff===null?'-':($diff<60?$diff.'s':($diff<3600?round($diff/60).'min':round($diff/3600).'h')).' atras';
|
||||
?>
|
||||
<tr>
|
||||
<td style="font-family:monospace;font-size:12px"><?= h($uid) ?></td><td style="font-size:12px;color:#8b949e"><?= ($d['fw']??'')!=='' ? h($d['fw']) : '—' ?></td>
|
||||
<td style="font-size:12px;color:var(--muted)"><?= h($d['fw_ver'] ?? '—') ?></td>
|
||||
<td><?php if($online):?><span class="badge" style="background:#0d2818;color:#3fb950">online</span><?php else:?><span class="badge" style="background:#1e2733;color:var(--muted)">offline</span><?php endif;?></td>
|
||||
<td><?php if($blocked):?><span class="badge" style="background:#2d1a1a;color:#f85149">bloqueado</span><?php elseif($auth_ok):?><span class="badge" style="background:#0d2818;color:#3fb950">aprovado</span><?php else:?><span class="badge" style="background:#1e2733;color:var(--muted)">pendente</span><?php endif;?></td>
|
||||
<td style="color:var(--muted);font-size:12px"><?= h($last) ?><br><?= $ago ?></td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<input type="range" min="0" max="255" value="<?= $bri ?>" id="bri_<?= h($uid) ?>" style="width:90px;accent-color:var(--accent)" oninput="setBri('<?= h($uid) ?>',this.value)" <?= $online?'':'disabled'?>>
|
||||
<span id="brival_<?= h($uid) ?>" style="font-size:11px;color:var(--muted);min-width:24px"><?= $bri ?></span>
|
||||
</div>
|
||||
</td>
|
||||
<td style="white-space:nowrap">
|
||||
<?php if($blocked):?>
|
||||
<button class="btn btn-secondary btn-sm" data-uid="<?= h($uid) ?>" data-act="approve">Aprovar</button>
|
||||
<?php else:?>
|
||||
<button class="btn btn-danger btn-sm" data-uid="<?= h($uid) ?>" data-act="block">Bloquear</button>
|
||||
<?php endif;?>
|
||||
<button class="btn btn-secondary btn-sm" data-uid="<?= h($uid) ?>" data-act="cmd" data-cmd="REBOOT">Reboot</button>
|
||||
<button class="btn btn-danger btn-sm" data-uid="<?= h($uid) ?>" data-act="delete">✕</button>
|
||||
</td>
|
||||
<td style="white-space:nowrap">
|
||||
<button class="btn btn-secondary btn-sm" data-uid="<?= h($uid) ?>" data-act="switch_pair" data-pair="0"><?= $isVps?$dot:'' ?>→ VPS</button>
|
||||
<button class="btn btn-secondary btn-sm" data-uid="<?= h($uid) ?>" data-act="switch_pair" data-pair="1"><?= $isPmx?$dot:'' ?>→ Proxmox</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach;?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif;?>
|
||||
|
||||
<script>
|
||||
const _bt={};
|
||||
function act(uid, action, extra){
|
||||
const fd=new FormData(); fd.append('uid',uid); fd.append('action',action);
|
||||
if(extra) Object.keys(extra).forEach(k=>fd.append(k,extra[k]));
|
||||
fetch('/esp_vps.php',{method:'POST',body:fd}).then(()=>location.reload());
|
||||
}
|
||||
document.addEventListener('click',function(e){
|
||||
const b=e.target.closest('button[data-act]'); if(!b) return;
|
||||
if(b.dataset.act==='delete' && !confirm('Remover '+b.dataset.uid+'?')) return;
|
||||
const extra={};
|
||||
if(b.dataset.pair!==undefined) extra.pair=b.dataset.pair;
|
||||
if(b.dataset.cmd!==undefined) extra.cmd=b.dataset.cmd;
|
||||
act(b.dataset.uid,b.dataset.act,extra);
|
||||
});
|
||||
function setBri(uid,val){
|
||||
document.getElementById('brival_'+uid).textContent=val;
|
||||
clearTimeout(_bt[uid]);
|
||||
_bt[uid]=setTimeout(()=>{
|
||||
const fd=new FormData();
|
||||
fd.append('action','brightness');fd.append('uid',uid);fd.append('value',val);
|
||||
fetch('/esp_vps.php',{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:fd});
|
||||
},400);
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php $content = ob_get_clean(); require __DIR__.'/inc/layout.php';
|
||||
20
inc/common.php
Normal file
20
inc/common.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header("Location: /api/login.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
function h($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
function db(): PDO {
|
||||
static $pdo;
|
||||
if (!$pdo) {
|
||||
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
94
inc/layout.php
Normal file
94
inc/layout.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php if (session_status() === PHP_SESSION_NONE) session_start(); ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title><?= isset($title) ? htmlspecialchars($title).' — Proxmox' : 'Proxmox' ?></title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#0d1117;--surface:#161b22;--surface2:#1c2128;--border:#30363d;
|
||||
--text:#e6edf3;--muted:#7d8590;--accent:#2f81f7;--accent-hover:#388bfd;
|
||||
--danger:#da3633;--danger-hover:#f85149;--success:#3fb950;--radius:8px;
|
||||
}
|
||||
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-size:14px;min-height:100vh}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{color:var(--accent-hover)}
|
||||
.header{background:var(--surface);border-bottom:1px solid var(--border);padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100}
|
||||
.header-brand{display:flex;align-items:center;gap:10px;font-weight:600;font-size:15px;color:var(--text)}
|
||||
.header-brand svg{color:var(--accent)}
|
||||
.header-nav{display:flex;align-items:center;gap:16px;color:var(--muted);font-size:13px}
|
||||
.header-nav strong{color:var(--text)}
|
||||
.header-nav a{color:var(--muted);padding:5px 10px;border-radius:6px;transition:.15s}
|
||||
.header-nav a:hover{background:var(--surface2);color:var(--text)}
|
||||
.page{max-width:1100px;margin:0 auto;padding:32px 24px}
|
||||
.breadcrumb{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px;margin-bottom:24px}
|
||||
.breadcrumb a{color:var(--muted)}.breadcrumb a:hover{color:var(--text)}.breadcrumb .sep{opacity:.4}
|
||||
.page-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}
|
||||
.page-header h1{font-size:22px;font-weight:600}
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
|
||||
.card-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.card-header h2{font-size:15px;font-weight:600}
|
||||
.card-body{padding:20px}
|
||||
.table-wrap{overflow-x:auto}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
thead tr{background:var(--surface2)}
|
||||
th{padding:11px 16px;text-align:left;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);border-bottom:1px solid var(--border)}
|
||||
td{padding:12px 16px;border-bottom:1px solid var(--border);vertical-align:middle}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
tbody tr:hover{background:var(--surface2)}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:600;letter-spacing:.04em}
|
||||
.badge-tcp{background:#1f3a5f;color:#58a6ff}.badge-udp{background:#1f3d2f;color:#3fb950}
|
||||
.btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;border:1px solid transparent;transition:.15s;text-decoration:none}
|
||||
.btn-primary{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||||
.btn-primary:hover{background:var(--accent-hover);color:#fff}
|
||||
.btn-secondary{background:var(--surface2);color:var(--text);border-color:var(--border)}
|
||||
.btn-secondary:hover{background:#2d333b;color:var(--text)}
|
||||
.btn-danger{background:transparent;color:var(--danger);border-color:#6e2120}
|
||||
.btn-danger:hover{background:#6e2120;color:#fff}
|
||||
.btn-sm{padding:4px 10px;font-size:12px}
|
||||
.form-group{margin-bottom:18px}
|
||||
.form-group label{display:block;font-size:13px;font-weight:500;color:var(--muted);margin-bottom:6px}
|
||||
.form-group input,.form-group select,.form-group textarea{width:100%;padding:9px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px;outline:none;transition:.15s}
|
||||
.form-group input:focus,.form-group select:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(47,129,247,.15)}
|
||||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||
.form-actions{display:flex;gap:10px;margin-top:24px}
|
||||
.alert{padding:12px 16px;border-radius:6px;font-size:13px;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
||||
.alert-error{background:#2d0f0e;border:1px solid #6e2120;color:#f85149}
|
||||
.alert-success{background:#0d2818;border:1px solid #1a4a2e;color:#3fb950}
|
||||
.empty-state{text-align:center;padding:60px 20px;color:var(--muted)}
|
||||
.nav-section{margin-bottom:28px}
|
||||
.nav-section-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||||
.nav-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px}
|
||||
.nav-item{display:flex;align-items:center;gap:12px;padding:14px 16px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);text-decoration:none;transition:.15s}
|
||||
.nav-item:hover{border-color:var(--accent);background:#1c2128;color:var(--text)}
|
||||
.nav-item .nav-ico{width:34px;height:34px;border-radius:8px;background:var(--surface);display:flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0}
|
||||
.nav-item .nav-label{font-size:13px;font-weight:500}
|
||||
.nav-item.danger{border-color:#6e2120}
|
||||
.nav-item.danger:hover{background:#2d0f0e;border-color:var(--danger)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="header-brand">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
|
||||
</svg>
|
||||
Proxmox
|
||||
</div>
|
||||
<div class="header-nav">
|
||||
<?php if (!empty($_SESSION['username'])): ?>
|
||||
<span>👤 <strong><?= htmlspecialchars($_SESSION['username']) ?></strong></span>
|
||||
<?php if (($title ?? '') !== 'Painel'): ?>
|
||||
<a href="/painel.php">← Painel</a>
|
||||
<?php endif; ?>
|
||||
<a href="/api/logout.php" style="color:var(--danger)">Sair</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page">
|
||||
<?= $content ?? '' ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
174
ota.php
Normal file
174
ota.php
Normal file
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
require_once __DIR__.'/inc/common.php';
|
||||
|
||||
$OTA_DIR = __DIR__.'/ota/';
|
||||
$MANIFEST = $OTA_DIR.'manifest.json';
|
||||
$MANIFEST_URL = (isset($_SERVER['HTTPS']) ? 'https' : 'http').'://'.$_SERVER['HTTP_HOST'].'/ota/manifest.json';
|
||||
|
||||
$msg = ''; $msgOk = true;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['fw'])) {
|
||||
$chip = preg_replace('/[^a-z0-9]/', '', strtolower($_POST['chip'] ?? ''));
|
||||
if (!in_array($chip, ['s3','wroom'], true)) {
|
||||
$msg = 'Chip invalido.'; $msgOk = false;
|
||||
} else {
|
||||
$dest = $OTA_DIR."fw_{$chip}.bin";
|
||||
if (move_uploaded_file($_FILES['fw']['tmp_name'], $dest)) {
|
||||
$manifest = file_exists($MANIFEST) ? json_decode(file_get_contents($MANIFEST), true) : [];
|
||||
$manifest[$chip] = (isset($_SERVER['HTTPS']) ? 'https' : 'http').'://'.$_SERVER['HTTP_HOST']."/ota/fw_{$chip}.bin";
|
||||
if (!empty($_POST['version'])) $manifest['version'] = trim($_POST['version']);
|
||||
ksort($manifest);
|
||||
file_put_contents($MANIFEST, json_encode($manifest, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
||||
$msg = "fw_{$chip}.bin guardado. Manifest atualizado.";
|
||||
} else {
|
||||
$msg = 'Erro ao guardar ficheiro.'; $msgOk = false;
|
||||
}
|
||||
}
|
||||
$qs = $msgOk ? '?ok='.urlencode($msg) : '?err='.urlencode($msg);
|
||||
header("Location: /ota.php$qs"); exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['ok'])) { $msg = htmlspecialchars($_GET['ok']); $msgOk = true; }
|
||||
if (isset($_GET['err'])) { $msg = htmlspecialchars($_GET['err']); $msgOk = false; }
|
||||
|
||||
$manifest = file_exists($MANIFEST) ? json_decode(file_get_contents($MANIFEST), true) : null;
|
||||
$latest = $manifest['version'] ?? null;
|
||||
|
||||
$devices = db()->query("
|
||||
SELECT uid, chip, fw_ver, online, blocked, last_seen,
|
||||
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago_s
|
||||
FROM contadores ORDER BY uid ASC
|
||||
")->fetchAll();
|
||||
|
||||
$title = "OTA Firmware";
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="breadcrumb"><a href="/painel.php">Painel</a><span class="sep">›</span>OTA</div>
|
||||
<h1>OTA Firmware</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($msg): ?>
|
||||
<div class="alert <?= $msgOk ? 'alert-success' : 'alert-error' ?>"><?= $msgOk ? '✓' : '⚠' ?> <?= $msg ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:24px">
|
||||
|
||||
<!-- Manifest -->
|
||||
<div class="card" style="padding:20px">
|
||||
<div style="font-weight:600;margin-bottom:14px;font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)">Manifest</div>
|
||||
<?php if ($manifest): ?>
|
||||
<div style="font-size:22px;font-weight:600;color:var(--success);margin-bottom:12px"><?= h($latest ?? '—') ?></div>
|
||||
<?php foreach (['s3','wroom'] as $chip): $f = $OTA_DIR."fw_{$chip}.bin"; ?>
|
||||
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:6px;padding:6px 10px;background:var(--surface2);border-radius:6px">
|
||||
<span style="color:var(--muted)"><?= $chip ?></span>
|
||||
<?php if (file_exists($f)): ?>
|
||||
<span><?= round(filesize($f)/1024/1024,2) ?> MB <span style="color:var(--muted)"><?= date('d/m H:i', filemtime($f)) ?></span></span>
|
||||
<?php else: ?><span style="color:var(--danger)">sem ficheiro</span><?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div style="margin-top:10px;font-size:10px;color:var(--muted);font-family:monospace;word-break:break-all"><?= h($MANIFEST_URL) ?></div>
|
||||
<?php else: ?>
|
||||
<div style="color:var(--muted);font-size:13px">Nenhum manifest ainda.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Upload S3 -->
|
||||
<div class="card" style="padding:20px">
|
||||
<div style="font-weight:600;margin-bottom:14px;font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)">Upload S3</div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="chip" value="s3">
|
||||
<div style="margin-bottom:10px">
|
||||
<label style="font-size:11px;color:var(--muted);display:block;margin-bottom:4px">Versao</label>
|
||||
<input name="version" type="text" placeholder="<?= h($latest ?? '1.0.10') ?>"
|
||||
style="background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-size:13px;width:100%">
|
||||
</div>
|
||||
<div style="margin-bottom:14px">
|
||||
<label style="font-size:11px;color:var(--muted);display:block;margin-bottom:4px">fw_s3.bin</label>
|
||||
<input name="fw" type="file" accept=".bin" style="font-size:12px;color:var(--text)">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="width:100%">Upload S3</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Upload WROOM -->
|
||||
<div class="card" style="padding:20px">
|
||||
<div style="font-weight:600;margin-bottom:14px;font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)">Upload WROOM</div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="chip" value="wroom">
|
||||
<div style="margin-bottom:10px">
|
||||
<label style="font-size:11px;color:var(--muted);display:block;margin-bottom:4px">Versao</label>
|
||||
<input name="version" type="text" placeholder="<?= h($latest ?? '1.0.10') ?>"
|
||||
style="background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-size:13px;width:100%">
|
||||
</div>
|
||||
<div style="margin-bottom:14px">
|
||||
<label style="font-size:11px;color:var(--muted);display:block;margin-bottom:4px">fw_wroom.bin</label>
|
||||
<input name="fw" type="file" accept=".bin" style="font-size:12px;color:var(--text)">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="width:100%">Upload WROOM</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Dispositivos -->
|
||||
<div class="card">
|
||||
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
||||
<h2>Dispositivos <span style="color:var(--muted);font-weight:400;font-size:13px">(<?= count($devices) ?>)</span></h2>
|
||||
<?php if ($manifest): ?>
|
||||
<button class="btn btn-primary btn-sm" onclick="otaAll()">⬆ Atualizar todos os desatualizados</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>UID</th><th>Chip</th><th>Versao atual</th><th>Disponivel</th><th>Online</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($devices as $d):
|
||||
$online = (bool)$d['online'];
|
||||
$fw = $d['fw_ver'] ?? null;
|
||||
$chip = $d['chip'] ?? null;
|
||||
$outdated = $latest && $fw && $fw !== $latest;
|
||||
?>
|
||||
<tr>
|
||||
<td style="font-family:monospace;font-size:12px"><?= h($d['uid']) ?></td>
|
||||
<td style="font-size:12px;color:var(--muted)"><?= h($chip ?? '—') ?></td>
|
||||
<td style="font-size:12px">
|
||||
<?php if ($fw): ?>
|
||||
<span style="color:<?= $outdated ? '#d29922' : 'var(--success)' ?>"><?= h($fw) ?></span>
|
||||
<?php else: ?><span style="color:var(--muted)">—</span><?php endif; ?>
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted)"><?= $latest ? h($latest) : '—' ?></td>
|
||||
<td><?php if ($online): ?><span class="badge" style="background:#0d2818;color:var(--success)">online</span><?php else: ?><span class="badge" style="background:#1e2733;color:var(--muted)">offline</span><?php endif; ?></td>
|
||||
<td>
|
||||
<?php if ($manifest && $online): ?>
|
||||
<button class="btn btn-secondary btn-sm" onclick="otaOne('<?= h($d['uid']) ?>')">OTA</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const MANIFEST_URL = <?= json_encode($MANIFEST_URL) ?>;
|
||||
function otaOne(uid) {
|
||||
fetch('/api/ota_trigger.php', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({uid, url: MANIFEST_URL, cmd:'OTA_CHECK'})
|
||||
}).then(r=>r.json()).then(r=>{ alert(r.ok ? 'OTA_CHECK enviado para '+uid : 'Erro: '+(r.error||'?')); });
|
||||
}
|
||||
function otaAll() {
|
||||
if (!confirm('Enviar OTA_CHECK a todos os dispositivos online?')) return;
|
||||
fetch('/api/ota_trigger.php', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({uid:'all', url: MANIFEST_URL, cmd:'OTA_CHECK'})
|
||||
}).then(r=>r.json()).then(r=>{ alert(r.ok ? 'OTA_CHECK enviado a '+r.sent+' dispositivos.' : 'Erro: '+(r.error||'?')); });
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php $content = ob_get_clean(); require __DIR__.'/inc/layout.php';
|
||||
86
painel.php
Normal file
86
painel.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
require_once __DIR__.'/inc/common.php';
|
||||
$title = "Painel";
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div style="max-width:640px;margin:0 auto">
|
||||
|
||||
<div style="margin-bottom:32px">
|
||||
<h1 style="font-size:22px;font-weight:600">Painel</h1>
|
||||
<p style="color:var(--muted);font-size:13px;margin-top:4px">Bem-vindo, <?= h($_SESSION['username'] ?? '') ?></p>
|
||||
</div>
|
||||
|
||||
<!-- ESP Devices -->
|
||||
<div class="dash-section">
|
||||
<div class="dash-section-label">ESP Devices</div>
|
||||
<div class="dash-grid">
|
||||
<a href="/esp_proxmox.php" class="dash-card">
|
||||
<div class="dash-ico">📡</div>
|
||||
<div class="dash-label">ESP Proxmox</div>
|
||||
<div class="dash-desc">Dispositivos locais</div>
|
||||
</a>
|
||||
<a href="/ota.php" class="dash-card">
|
||||
<div class="dash-ico">⬆</div>
|
||||
<div class="dash-label">OTA</div>
|
||||
<div class="dash-desc">Firmware updates</div>
|
||||
</a>
|
||||
<a href="/esp_vps.php" class="dash-card">
|
||||
<div class="dash-ico">📡</div>
|
||||
<div class="dash-label">ESP VPS</div>
|
||||
<div class="dash-desc">Dispositivos VPS</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rede -->
|
||||
<div class="dash-section">
|
||||
<div class="dash-section-label">Rede</div>
|
||||
<div class="dash-grid">
|
||||
<a href="/vpn.php" class="dash-card">
|
||||
<div class="dash-ico">🌍</div>
|
||||
<div class="dash-label">VPN</div>
|
||||
<div class="dash-desc">NordVPN · WireGuard</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Infraestrutura -->
|
||||
<div class="dash-section">
|
||||
<div class="dash-section-label">Infraestrutura</div>
|
||||
<div class="dash-grid">
|
||||
<a href="/proxmox.php" class="dash-card">
|
||||
<div class="dash-ico">⚙️</div>
|
||||
<div class="dash-label">Proxmox</div>
|
||||
<div class="dash-desc">LXCs · VMs · Storage</div>
|
||||
</a>
|
||||
<a href="/matrix_admin.php" class="dash-card">
|
||||
<div class="dash-ico">💬</div>
|
||||
<div class="dash-label">Matrix</div>
|
||||
<div class="dash-desc">Admin</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/api/logout.php" class="dash-card dash-danger" style="max-width:180px">
|
||||
<div class="dash-ico">→</div>
|
||||
<div class="dash-label">Sair</div>
|
||||
<div class="dash-desc"></div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dash-section{margin-bottom:28px}
|
||||
.dash-section-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||||
.dash-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px}
|
||||
.dash-card{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:8px;padding:22px 16px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);text-decoration:none;transition:.15s}
|
||||
.dash-card:hover{border-color:var(--accent);background:var(--surface2);color:var(--text)}
|
||||
.dash-ico{font-size:26px;line-height:1}
|
||||
.dash-label{font-size:13px;font-weight:600}
|
||||
.dash-desc{font-size:11px;color:var(--muted)}
|
||||
.dash-danger{border-color:#6e2120}
|
||||
.dash-danger:hover{background:#2d0f0e;border-color:var(--danger)}
|
||||
</style>
|
||||
|
||||
<?php $content = ob_get_clean(); require __DIR__.'/inc/layout.php';
|
||||
Loading…
Reference in New Issue
Block a user