www/ota.php

175 lines
8.3 KiB
PHP

<?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">&#x203A;</span>OTA</div>
<h1>OTA Firmware</h1>
</div>
</div>
<?php if ($msg): ?>
<div class="alert <?= $msgOk ? 'alert-success' : 'alert-error' ?>"><?= $msgOk ? '&#x2713;' : '&#x26A0;' ?> <?= $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 &nbsp;<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()">&#x2B06; 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';