diff --git a/web/app.rb b/web/app.rb index 1d30471..c995aff 100644 --- a/web/app.rb +++ b/web/app.rb @@ -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 diff --git a/web/public/index.html b/web/public/index.html index b2ce9ee..d37e8c1 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -1,4 +1,3 @@ - @@ -49,7 +48,7 @@

Meshtastic Berlin

- #MediumFast — auto-refresh every 60 seconds. + loading…
@@ -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 = ` ${n.node_id || ""} ${n.short_name || ""} ${n.long_name || ""} - ${timeAgo(n.last_heard)} + ${timeAgo(n.last_heard, nowSec)} ${n.role || "CLIENT"} ${fmtHw(n.hw_model)} ${fmtAlt(n.battery_level, "%")} @@ -233,18 +229,20 @@ ${fmtCoords(n.latitude)} ${fmtCoords(n.longitude)} ${fmtAlt(n.altitude, "m")} - ${n.pos_time_iso ? `${timeAgo(n.position_time)}` : ""}`; - tb.appendChild(tr); + ${n.pos_time_iso ? `${timeAgo(n.position_time, nowSec)}` : ""}`; + 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('
')); @@ -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) {