Optimize web frontend and Ruby app (#32)

* Optimize web app and cleanup

* Refine node rendering and front-end timing
This commit is contained in:
l5y
2025-09-14 19:31:28 +02:00
committed by GitHub
parent a1cabec150
commit 334e21d674
2 changed files with 43 additions and 42 deletions
+8 -6
View File
@@ -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
View File
@@ -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) {