mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-08 09:52:44 +02:00
Optimize web frontend and Ruby app (#32)
* Optimize web app and cleanup * Refine node rendering and front-end timing
This commit is contained in:
+8
-6
@@ -5,13 +5,14 @@ require "sqlite3"
|
||||
|
||||
# run ../data/mesh.sh to populate nodes and messages database
|
||||
DB_PATH = ENV.fetch("MESH_DB", File.join(__dir__, "../data/mesh.db"))
|
||||
WEEK_SECONDS = 7 * 24 * 60 * 60
|
||||
|
||||
set :public_folder, File.join(__dir__, "public")
|
||||
|
||||
def query_nodes(limit)
|
||||
db = SQLite3::Database.new(DB_PATH)
|
||||
db.results_as_hash = true
|
||||
min_last_heard = Time.now.to_i - 7 * 24 * 60 * 60
|
||||
db = SQLite3::Database.new(DB_PATH, readonly: true, results_as_hash: true)
|
||||
now = Time.now.to_i
|
||||
min_last_heard = now - WEEK_SECONDS
|
||||
rows = db.execute <<~SQL, [min_last_heard, limit]
|
||||
SELECT node_id, short_name, long_name, hw_model, role, snr,
|
||||
battery_level, voltage, last_heard, first_heard,
|
||||
@@ -24,9 +25,10 @@ def query_nodes(limit)
|
||||
SQL
|
||||
rows.each do |r|
|
||||
r["role"] ||= "CLIENT"
|
||||
lh = r["last_heard"]; pt = r["position_time"]
|
||||
r["last_seen_iso"] = lh ? Time.at(lh.to_i).utc.iso8601 : nil
|
||||
r["pos_time_iso"] = pt ? Time.at(pt.to_i).utc.iso8601 : nil
|
||||
lh = r["last_heard"]
|
||||
pt = r["position_time"]
|
||||
r["last_seen_iso"] = Time.at(lh.to_i).utc.iso8601 if lh
|
||||
r["pos_time_iso"] = Time.at(pt.to_i).utc.iso8601 if pt
|
||||
end
|
||||
rows
|
||||
ensure
|
||||
|
||||
+35
-36
@@ -1,4 +1,3 @@
|
||||
</html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -49,7 +48,7 @@
|
||||
<h1>Meshtastic Berlin</h1>
|
||||
<div class="row meta">
|
||||
<div>
|
||||
<span>#MediumFast — auto-refresh every 60 seconds.</span>
|
||||
<span id="refreshInfo"></span>
|
||||
<span id="status" class="pill">loading…</span>
|
||||
<button id="refreshBtn" type="button">Refresh now</button>
|
||||
</div>
|
||||
@@ -101,11 +100,15 @@
|
||||
const titleEl = document.querySelector('title');
|
||||
const headerEl = document.querySelector('h1');
|
||||
const chatEl = document.getElementById('chat');
|
||||
const refreshInfo = document.getElementById('refreshInfo');
|
||||
const baseTitle = document.title;
|
||||
let allNodes = [];
|
||||
const seenNodeIds = new Set();
|
||||
const NODE_LIMIT = 1000;
|
||||
const REFRESH_MS = 60000;
|
||||
refreshInfo.textContent = `#MediumFast — auto-refresh every ${REFRESH_MS / 1000} seconds.`;
|
||||
|
||||
const roleColors = {
|
||||
const roleColors = Object.freeze({
|
||||
CLIENT: '#A8D5BA',
|
||||
CLIENT_HIDDEN: '#B8DCA9',
|
||||
CLIENT_MUTE: '#D2E3A2',
|
||||
@@ -115,7 +118,7 @@
|
||||
REPEATER: '#F7B7A3',
|
||||
ROUTER_LATE: '#F29AA3',
|
||||
ROUTER: '#E88B94'
|
||||
};
|
||||
});
|
||||
|
||||
// --- Map setup ---
|
||||
const map = L.map('map', { worldCopyJump: true });
|
||||
@@ -162,30 +165,23 @@
|
||||
}
|
||||
|
||||
function fmtHw(v) {
|
||||
if (v == null) return "";
|
||||
if (v == "UNSET") return "";
|
||||
return String(v);
|
||||
return v && v !== "UNSET" ? String(v) : "";
|
||||
}
|
||||
|
||||
function fmtCoords(v, d = 5) {
|
||||
if (v == null) return "";
|
||||
if (v == null || v === '') return "";
|
||||
const n = Number(v);
|
||||
return Number.isNaN(n) ? "" : n.toFixed(d);
|
||||
return Number.isFinite(n) ? n.toFixed(d) : "";
|
||||
}
|
||||
|
||||
function fmtAlt(v, s) {
|
||||
if (v == null) return "";
|
||||
if (v == 0) return "";
|
||||
const n = String(v) + String(s);
|
||||
return n;
|
||||
return (v == null || v === '') ? "" : `${v}${s}`;
|
||||
}
|
||||
|
||||
function fmtTx(v, d = 3) {
|
||||
if (v == null) return "";
|
||||
let n = Number(v);
|
||||
n = Number.isNaN(n) ? "" : n.toFixed(d);
|
||||
n = String(n) + "%";
|
||||
return n;
|
||||
if (v == null || v === '') return "";
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? `${n.toFixed(d)}%` : "";
|
||||
}
|
||||
|
||||
function timeHum(unixSec) {
|
||||
@@ -197,9 +193,9 @@
|
||||
return `${Math.floor(unixSec/86400)}d ${Math.floor((unixSec%86400)/3600)}h`;
|
||||
}
|
||||
|
||||
function timeAgo(unixSec) {
|
||||
function timeAgo(unixSec, nowSec = Date.now()/1000) {
|
||||
if (!unixSec) return "";
|
||||
const diff = Math.floor(Date.now()/1000 - Number(unixSec));
|
||||
const diff = Math.floor(nowSec - Number(unixSec));
|
||||
if (diff < 0) return "0s";
|
||||
if (diff < 60) return `${diff}s`;
|
||||
if (diff < 3600) return `${Math.floor(diff/60)}m ${Math.floor((diff%60))}s`;
|
||||
@@ -207,22 +203,22 @@
|
||||
return `${Math.floor(diff/86400)}d ${Math.floor((diff%86400)/3600)}h`;
|
||||
}
|
||||
|
||||
async function fetchNodes() {
|
||||
const r = await fetch('/api/nodes?limit=1000', { cache: 'no-store' });
|
||||
async function fetchNodes(limit = NODE_LIMIT) {
|
||||
const r = await fetch(`/api/nodes?limit=${limit}`, { cache: 'no-store' });
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function renderTable(nodes) {
|
||||
function renderTable(nodes, nowSec) {
|
||||
const tb = document.querySelector('#nodes tbody');
|
||||
tb.innerHTML = '';
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const n of nodes) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td class="mono">${n.node_id || ""}</td>
|
||||
<td>${n.short_name || ""}</td>
|
||||
<td>${n.long_name || ""}</td>
|
||||
<td>${timeAgo(n.last_heard)}</td>
|
||||
<td>${timeAgo(n.last_heard, nowSec)}</td>
|
||||
<td>${n.role || "CLIENT"}</td>
|
||||
<td>${fmtHw(n.hw_model)}</td>
|
||||
<td>${fmtAlt(n.battery_level, "%")}</td>
|
||||
@@ -233,18 +229,20 @@
|
||||
<td>${fmtCoords(n.latitude)}</td>
|
||||
<td>${fmtCoords(n.longitude)}</td>
|
||||
<td>${fmtAlt(n.altitude, "m")}</td>
|
||||
<td class="mono">${n.pos_time_iso ? `${timeAgo(n.position_time)}` : ""}</td>`;
|
||||
tb.appendChild(tr);
|
||||
<td class="mono">${n.pos_time_iso ? `${timeAgo(n.position_time, nowSec)}` : ""}</td>`;
|
||||
frag.appendChild(tr);
|
||||
}
|
||||
tb.replaceChildren(frag);
|
||||
}
|
||||
|
||||
function renderMap(nodes) {
|
||||
function renderMap(nodes, nowSec) {
|
||||
markersLayer.clearLayers();
|
||||
const pts = [];
|
||||
for (const n of nodes) {
|
||||
if (n.latitude == null || n.longitude == null) continue;
|
||||
const lat = Number(n.latitude), lon = Number(n.longitude);
|
||||
if (Number.isNaN(lat) || Number.isNaN(lon)) continue;
|
||||
const latRaw = n.latitude, lonRaw = n.longitude;
|
||||
if (latRaw == null || latRaw === '' || lonRaw == null || lonRaw === '') continue;
|
||||
const lat = Number(latRaw), lon = Number(lonRaw);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
|
||||
|
||||
const color = roleColors[n.role] || '#3388ff';
|
||||
const marker = L.circleMarker([lat, lon], {
|
||||
@@ -261,8 +259,8 @@
|
||||
n.hw_model ? `Model: ${fmtHw(n.hw_model)}` : null,
|
||||
`Role: ${n.role || 'CLIENT'}`,
|
||||
(n.battery_level != null ? `Battery: ${fmtAlt(n.battery_level, "%")}, ${fmtAlt(n.voltage, "V")}` : null),
|
||||
(n.last_heard ? `Last seen: ${timeAgo(n.last_heard)}` : null),
|
||||
(n.pos_time_iso ? `Last Position: ${timeAgo(n.position_time)}` : null),
|
||||
(n.last_heard ? `Last seen: ${timeAgo(n.last_heard, nowSec)}` : null),
|
||||
(n.pos_time_iso ? `Last Position: ${timeAgo(n.position_time, nowSec)}` : null),
|
||||
(n.uptime_seconds ? `Uptime: ${timeHum(n.uptime_seconds)}` : null),
|
||||
].filter(Boolean);
|
||||
marker.bindPopup(lines.join('<br/>'));
|
||||
@@ -282,8 +280,9 @@
|
||||
.filter(Boolean)
|
||||
.some(v => v.toLowerCase().includes(q));
|
||||
});
|
||||
renderTable(nodes);
|
||||
renderMap(nodes);
|
||||
const nowSec = Date.now()/1000;
|
||||
renderTable(nodes, nowSec);
|
||||
renderMap(nodes, nowSec);
|
||||
updateCount(nodes.length);
|
||||
}
|
||||
|
||||
@@ -314,7 +313,7 @@
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 60000);
|
||||
setInterval(refresh, REFRESH_MS);
|
||||
refreshBtn.addEventListener('click', refresh);
|
||||
|
||||
function updateCount(count) {
|
||||
|
||||
Reference in New Issue
Block a user