diff --git a/web/app.rb b/web/app.rb
index 210367a..450b594 100644
--- a/web/app.rb
+++ b/web/app.rb
@@ -13,8 +13,10 @@ def query_nodes(limit)
db.results_as_hash = true
min_last_heard = Time.now.to_i - 7 * 24 * 60 * 60
rows = db.execute <<~SQL, [min_last_heard, limit]
- SELECT node_id, short_name, long_name, hw_model, role, snr, battery_level,
- last_heard, position_time, latitude, longitude, altitude
+ SELECT node_id, short_name, long_name, hw_model, role, snr,
+ battery_level, voltage, last_heard, uptime_seconds,
+ channel_utilization, air_util_tx, position_time,
+ latitude, longitude, altitude
FROM nodes
WHERE last_heard >= ?
ORDER BY last_heard DESC
diff --git a/web/public/index.html b/web/public/index.html
index 326cb0d..3608b6c 100644
--- a/web/public/index.html
+++ b/web/public/index.html
@@ -33,7 +33,7 @@
.row { display: flex; gap: var(--pad); align-items: center; justify-content: space-between; }
.map-row { display: flex; gap: var(--pad); align-items: stretch; }
#chat { flex: 0 0 33%; max-width: 33%; height: 60vh; border: 1px solid #ddd; border-radius: 8px; overflow-y: auto; padding: 6px; font-size: 12px; }
- .chat-entry { margin-bottom: 4px; font-family: ui-monospace, Menlo, Consolas, monospace; }
+ .chat-entry-node { font-family: ui-monospace, Menlo, Consolas, monospace; color: #555 }
.controls { display: flex; gap: 8px; align-items: center; }
button { padding: 6px 10px; border: 1px solid #ccc; background: #fff; border-radius: 6px; cursor: pointer; }
button:hover { background: #f6f6f6; }
@@ -70,12 +70,17 @@
Node ID |
Short |
Long Name |
- HW Model |
- Role |
- Battery |
Last Seen |
- Lat |
- Lon |
+ Role |
+ HW Model |
+ Battery |
+ Voltage |
+ Uptime |
+ Channel Util |
+ Air Util Tx |
+ Latitude |
+ Longitude |
+ Altitude |
Last Position |
@@ -135,15 +140,18 @@
// --- Helpers ---
function renderShortHtml(short){ return String(short).padStart(4, ' ').replace(/ /g, ' '); }
+
function addNewNodeChatEntry(n) {
const div = document.createElement('div');
const ts = formatDate(new Date(n.last_heard * 1000));
- div.className = 'chat-entry';
+ div.className = 'chat-entry-node';
div.innerHTML = `[${ts}] ${n.node_id || ''} ${renderShortHtml(n.short_name || '')} New node: ${n.long_name || ''}`;
chatEl.appendChild(div);
chatEl.scrollTop = chatEl.scrollHeight;
}
+
function pad(n) { return String(n).padStart(2, "0"); }
+
function formatDate(d) {
return d.getFullYear() + "-" +
pad(d.getMonth() + 1) + "-" +
@@ -152,20 +160,45 @@
pad(d.getMinutes()) + ":" +
pad(d.getSeconds());
}
- function fmt(v, d = 5) {
+
+ function fmtCoords(v, d = 5) {
if (v == null) return "";
const n = Number(v);
return Number.isNaN(n) ? "" : n.toFixed(d);
}
+ function fmtAlt(v, s) {
+ if (v == null) return "";
+ if (v == 0) return "";
+ const n = String(v) + String(s);
+ return n;
+ }
+
+ 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;
+ }
+
+ function timeHum(unixSec) {
+ if (!unixSec) return "";
+ if (unixSec < 0) return "0s";
+ if (unixSec < 60) return `${unixSec}s`;
+ if (unixSec < 3600) return `${Math.floor(unixSec/60)}m ${Math.floor((unixSec%60))}s`;
+ if (unixSec < 86400) return `${Math.floor(unixSec/3600)}h ${Math.floor((unixSec%3600)/60)}m`;
+ return `${Math.floor(unixSec/86400)}d ${Math.floor((unixSec%86400)/3600)}h`;
+ }
+
function timeAgo(unixSec) {
if (!unixSec) return "";
const diff = Math.floor(Date.now()/1000 - Number(unixSec));
- if (diff < 0) return "0s ago";
- if (diff < 60) return `${diff}s ago`;
- if (diff < 3600) return `${Math.floor(diff/60)}m ago`;
- if (diff < 86400) return `${Math.floor(diff/3600)}h ${Math.floor((diff%3600)/60)}m ago`;
- return `${Math.floor(diff/86400)}d ago`;
+ 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`;
+ if (diff < 86400) return `${Math.floor(diff/3600)}h ${Math.floor((diff%3600)/60)}m`;
+ return `${Math.floor(diff/86400)}d ${Math.floor((diff%86400)/3600)}h`;
}
async function fetchNodes() {
@@ -183,12 +216,17 @@
${n.node_id || ""} |
${n.short_name || ""} |
${n.long_name || ""} |
- ${n.hw_model || ""} |
- ${n.role || "CLIENT"} |
- ${n.battery_level ?? ""} |
${timeAgo(n.last_heard)} |
- ${fmt(n.latitude)} |
- ${fmt(n.longitude)} |
+ ${n.role || "CLIENT"} |
+ ${n.hw_model || ""} |
+ ${fmtAlt(n.battery_level, "%")} |
+ ${fmtAlt(n.voltage, "V")} |
+ ${timeHum(n.uptime_seconds)} |
+ ${fmtTx(n.channel_utilization)} |
+ ${fmtTx(n.air_util_tx)} |
+ ${fmtCoords(n.latitude)} |
+ ${fmtCoords(n.longitude)} |
+ ${fmtAlt(n.altitude, "m")} |
${n.pos_time_iso ? `${timeAgo(n.position_time)}` : ""} | `;
tb.appendChild(tr);
}