250 lines
14 KiB
PHP
250 lines
14 KiB
PHP
<?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, chip, clock_style, clock_theme,
|
||
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') {
|
||
// ações de frota (broadcast) — não precisam de uid válido
|
||
$action0 = $_POST['action'] ?? '';
|
||
if ($action0 === 'clock_style' || $action0 === 'clock_theme') {
|
||
$res = pmx_post($_POST);
|
||
$who = ($_POST['uid'] ?? '') === 'all' ? 'frota toda' : ($_POST['uid'] ?? '?');
|
||
if (!empty($res['ok'])) {
|
||
$msg = "Relógio atualizado ($who)."; $msgOk = true;
|
||
// guardar estado no MySQL local p/ os dropdowns refletirem
|
||
try {
|
||
$pdo = new PDO('mysql:host=localhost;dbname=cagalhao;charset=utf8',
|
||
'master', 'master', [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
|
||
$col = $action0 === 'clock_style' ? 'clock_style' : 'clock_theme';
|
||
$val = $_POST['value'] ?? '';
|
||
if (($_POST['uid'] ?? '') === 'all') {
|
||
$pdo->prepare("UPDATE contadores SET $col=?")->execute([$val]);
|
||
} else {
|
||
$pdo->prepare("UPDATE contadores SET $col=? WHERE uid=?")->execute([$val, $_POST['uid']]);
|
||
}
|
||
} catch (Throwable $e) { /* estado é cosmético, não falhar por isto */ }
|
||
}
|
||
else { $msg = $res['error'] ?? 'Erro'; $msgOk = false; }
|
||
$qs = $msgOk ? '?ok='.urlencode($msg) : '?err='.urlencode($msg);
|
||
header("Location: /esp_proxmox.php$qs"); exit;
|
||
}
|
||
$uid = preg_replace('/[^0-9A-Fa-f]/', '', $_POST['uid'] ?? '');
|
||
if (!$uid) { $msg = 'UID invalido.'; $msgOk = false; }
|
||
else {
|
||
$action = $_POST['action'] ?? '';
|
||
try {
|
||
$pdo = new PDO('mysql:host=localhost;dbname=cagalhao;charset=utf8',
|
||
'master', 'master',
|
||
[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
|
||
if ($action === 'approve') {
|
||
$pdo->prepare("UPDATE contadores SET blocked=0 WHERE uid=?")->execute([$uid]);
|
||
// sincronizar na VPS
|
||
$vps = ['http'=>['method'=>'POST','header'=>'Content-Type: application/x-www-form-urlencoded',
|
||
'content'=>http_build_query(['action'=>'approve','uid'=>$uid]),'timeout'=>4]];
|
||
@file_get_contents('https://web.xpto.ooguy.com/esp_api.php?key=esp_api_xupas_2025', false, stream_context_create($vps));
|
||
$msg = "[$uid] aprovado OK"; $msgOk = true;
|
||
} elseif ($action === 'block') {
|
||
$pdo->prepare("UPDATE contadores SET blocked=1,online=0 WHERE uid=?")->execute([$uid]);
|
||
// sincronizar na VPS
|
||
$vps = ['http'=>['method'=>'POST','header'=>'Content-Type: application/x-www-form-urlencoded',
|
||
'content'=>http_build_query(['action'=>'block','uid'=>$uid]),'timeout'=>4]];
|
||
@file_get_contents('https://web.xpto.ooguy.com/esp_api.php?key=esp_api_xupas_2025', false, stream_context_create($vps));
|
||
$msg = "[$uid] bloqueado OK"; $msgOk = true;
|
||
} elseif ($action === 'delete') {
|
||
// CT106 api.py: envia DELETE_CERT via MQTT + apaga CT106 SQLite + apaga VPS SQLite
|
||
pmx_post(['uid'=>$uid,'action'=>'delete']);
|
||
// apagar também do CT102 MySQL (esta BD)
|
||
$pdo->prepare("DELETE FROM contadores WHERE uid=?")->execute([$uid]);
|
||
$msg = "[$uid] eliminado OK"; $msgOk = true;
|
||
} else {
|
||
// cmd, switch_pair, brightness — envia via CT106 api.py (MQTT)
|
||
$res = pmx_post($_POST);
|
||
if (!empty($res['ok'])) { $msg = "[$uid] $action OK"; $msgOk = true; }
|
||
else { $msg = $res['error'] ?? 'Erro'; $msgOk = false; }
|
||
}
|
||
} catch (Throwable $e) {
|
||
$msg = 'Erro BD: '.$e->getMessage(); $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; ?>
|
||
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap">
|
||
<span style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;">Relógio:</span>
|
||
<?php foreach (['Hybrid','Analógico','Barras','Big'] as $s => $lbl): ?>
|
||
<form method="post" style="display:inline">
|
||
<input type="hidden" name="uid" value="all">
|
||
<input type="hidden" name="action" value="clock_style">
|
||
<input type="hidden" name="value" value="<?= $s ?>">
|
||
<button type="submit" class="btn btn-secondary btn-sm"><?= $lbl ?></button>
|
||
</form>
|
||
<?php endforeach; ?>
|
||
<span style="width:1px;height:18px;background:#30363d;margin:0 4px"></span>
|
||
<span style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;">Tema:</span>
|
||
<?php foreach (['ambar'=>['Âmbar','#EF9F27'],'verde'=>['Verde','#3fb950'],'branco'=>['Branco','#d7dde4'],'noite'=>['Noite','#da3633']] as $tk => [$tlbl,$tcol]): ?>
|
||
<form method="post" style="display:inline">
|
||
<input type="hidden" name="uid" value="all">
|
||
<input type="hidden" name="action" value="clock_theme">
|
||
<input type="hidden" name="value" value="<?= $tk ?>">
|
||
<button type="submit" class="btn btn-secondary btn-sm" style="display:inline-flex;align-items:center;gap:6px"><span style="width:9px;height:9px;border-radius:50%;background:<?= $tcol ?>;display:inline-block"></span><?= $tlbl ?></button>
|
||
</form>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<?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>Ultimo contacto</th><th>Online</th><th>Estado</th><th>Brilho</th><th>Relógio</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']) : '—' ?><?php $ch=$d['chip']??''; echo $ch==='s3'?' <span class="badge" style="background:#0d1f3c;color:#58a6ff;font-size:10px">S3</span>':($ch==='wroom'?' <span class="badge" style="background:#1e2733;color:#8b949e;font-size:10px">WROOM</span>':''); ?></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>
|
||
<?php $levels = [1,2,4,8,12,16,24,32,48,64,96,128,176,224,255];
|
||
if (!in_array($bri, $levels, true)) { $levels[] = $bri; sort($levels); } ?>
|
||
<select onchange="setBri('<?= h($uid) ?>', this.value)" <?= $online?'':'disabled'?>
|
||
style="background:#1e2733;color:#c9d1d9;border:1px solid #30363d;border-radius:4px;padding:3px 6px;font-size:12px;">
|
||
<?php foreach ($levels as $lv): ?>
|
||
<option value="<?= $lv ?>" <?= $lv === $bri ? 'selected' : '' ?>><?= $lv ?><?= $lv===1?' (mín)':($lv===255?' (máx)':'') ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</td>
|
||
<td style="white-space:nowrap">
|
||
<?php $cs = $d['clock_style'] ?? null; $ct = $d['clock_theme'] ?? ''; ?>
|
||
<select onchange="clockCmd('<?= h($uid) ?>','clock_style',this.value)" <?= $online?'':'disabled'?>
|
||
style="background:#1e2733;color:#c9d1d9;border:1px solid #30363d;border-radius:4px;padding:3px 4px;font-size:11px;">
|
||
<option value="" <?= $cs === null || $cs === '' ? 'selected' : '' ?> disabled>Estilo…</option>
|
||
<?php foreach (['Hybrid','Analógico','Barras','Big'] as $si => $sl): ?>
|
||
<option value="<?= $si ?>" <?= (string)$cs === (string)$si ? 'selected' : '' ?>><?= $sl ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<select onchange="clockCmd('<?= h($uid) ?>','clock_theme',this.value)" <?= $online?'':'disabled'?>
|
||
style="background:#1e2733;color:#c9d1d9;border:1px solid #30363d;border-radius:4px;padding:3px 4px;font-size:11px;">
|
||
<option value="" <?= $ct === '' || $ct === null ? 'selected' : '' ?> disabled>Tema…</option>
|
||
<?php foreach (['ambar'=>'Âmbar','verde'=>'Verde','branco'=>'Branco','noite'=>'Noite'] as $tk => $tl): ?>
|
||
<option value="<?= $tk ?>" <?= $ct === $tk ? 'selected' : '' ?>><?= $tl ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</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;?>
|
||
<span data-uid="<?= h($uid) ?>" data-act="cmd" data-cmd="REBOOT" title="Reboot" style="cursor:pointer;font-size:10px;font-weight:700;color:var(--muted);border:1px solid #333;border-radius:4px;padding:1px 5px">RBT</span>
|
||
<button class="btn btn-danger btn-sm" data-uid="<?= h($uid) ?>" data-act="delete">✕</button>
|
||
</td>
|
||
<td style="text-align:center">
|
||
<span title="VPS" data-uid="<?= h($uid) ?>" data-act="switch_pair" data-pair="0"
|
||
style="cursor:pointer;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-size:9px;font-weight:700;margin:0 3px;background:<?= $isVps?'#3fb950':'#333' ?>;color:<?= $isVps?'#0d1117':'#666' ?>;<?= $isVps?'box-shadow:0 0 5px #3fb950;':'' ?>">V</span>
|
||
<span title="Proxmox" data-uid="<?= h($uid) ?>" data-act="switch_pair" data-pair="1"
|
||
style="cursor:pointer;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-size:9px;font-weight:700;margin:0 3px;background:<?= $isPmx?'#3fb950':'#333' ?>;color:<?= $isPmx?'#0d1117':'#666' ?>;<?= $isPmx?'box-shadow:0 0 5px #3fb950;':'' ?>">P</span>
|
||
</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('[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){
|
||
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});
|
||
}
|
||
function clockCmd(uid,action,val){
|
||
if(val==='') return;
|
||
const fd=new FormData();
|
||
fd.append('action',action);fd.append('uid',uid);fd.append('value',val);
|
||
fetch('/esp_proxmox.php',{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:fd}).catch(console.error);
|
||
}
|
||
</script>
|
||
|
||
<?php $content = ob_get_clean(); require __DIR__.'/inc/layout.php';
|