mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-25 02:35:17 +00:00
fix: observers — refresh a11y, table caption, spark ARIA, mobile, timezone (closes #93, #94, #95, #96, #97)
This commit is contained in:
+7
-2
@@ -9,9 +9,14 @@
|
||||
let autoScroll = true;
|
||||
let nodeCache = {};
|
||||
let selectedNode = null;
|
||||
var _nodeCacheTTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
async function lookupNode(name) {
|
||||
if (nodeCache[name] !== undefined) return nodeCache[name];
|
||||
var cached = nodeCache[name];
|
||||
if (cached !== undefined) {
|
||||
if (cached && cached.fetchedAt && (Date.now() - cached.fetchedAt < _nodeCacheTTL)) return cached.data;
|
||||
if (cached && !cached.fetchedAt) return cached; // legacy null entries
|
||||
}
|
||||
try {
|
||||
const data = await api('/nodes/search?q=' + encodeURIComponent(name));
|
||||
// Try exact match first, then case-insensitive, then contains
|
||||
@@ -20,7 +25,7 @@
|
||||
|| nodes.find(n => n.name && n.name.toLowerCase() === name.toLowerCase())
|
||||
|| nodes.find(n => n.name && n.name.toLowerCase().includes(name.toLowerCase()))
|
||||
|| nodes[0] || null;
|
||||
nodeCache[name] = match;
|
||||
nodeCache[name] = { data: match, fetchedAt: Date.now() };
|
||||
return match;
|
||||
} catch { nodeCache[name] = null; return null; }
|
||||
}
|
||||
|
||||
+10
-5
@@ -11,7 +11,7 @@
|
||||
<div class="observers-page">
|
||||
<div class="page-header">
|
||||
<h2>Observer Status</h2>
|
||||
<button class="btn-icon" data-action="obs-refresh" title="Refresh">🔄</button>
|
||||
<button class="btn-icon" data-action="obs-refresh" title="Refresh" aria-label="Refresh observers">🔄</button>
|
||||
</div>
|
||||
<div id="obsContent"><div class="text-center text-muted" style="padding:40px">Loading…</div></div>
|
||||
</div>`;
|
||||
@@ -47,11 +47,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Comparing server timestamps to Date.now() can skew if client/server
|
||||
// clocks differ. We add ±30s tolerance to thresholds to reduce false positives.
|
||||
function healthStatus(lastSeen) {
|
||||
if (!lastSeen) return { cls: 'health-red', label: 'Unknown' };
|
||||
const ago = Date.now() - new Date(lastSeen).getTime();
|
||||
if (ago < 600000) return { cls: 'health-green', label: 'Online' }; // < 10 min
|
||||
if (ago < 3600000) return { cls: 'health-yellow', label: 'Stale' }; // < 1 hour
|
||||
const tolerance = 30000; // 30s tolerance for clock skew
|
||||
if (ago < 600000 + tolerance) return { cls: 'health-green', label: 'Online' }; // < 10 min + tolerance
|
||||
if (ago < 3600000 + tolerance) return { cls: 'health-yellow', label: 'Stale' }; // < 1 hour + tolerance
|
||||
return { cls: 'health-red', label: 'Offline' };
|
||||
}
|
||||
|
||||
@@ -66,9 +69,10 @@
|
||||
}
|
||||
|
||||
function sparkBar(count, max) {
|
||||
if (max === 0) return '<div class="spark-bar"><div class="spark-fill" style="width:0"></div></div>';
|
||||
const aria = `role="meter" aria-valuenow="${count}" aria-valuemin="0" aria-valuemax="${max}" aria-label="Packet rate"`;
|
||||
if (max === 0) return `<div class="spark-bar" ${aria}><div class="spark-fill" style="width:0"></div></div>`;
|
||||
const pct = Math.min(100, Math.round((count / max) * 100));
|
||||
return `<div class="spark-bar"><div class="spark-fill" style="width:${pct}%"></div><span class="spark-label">${count}/hr</span></div>`;
|
||||
return `<div class="spark-bar" ${aria}><div class="spark-fill" style="width:${pct}%"></div><span class="spark-label">${count}/hr</span></div>`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
@@ -95,6 +99,7 @@
|
||||
<span class="obs-stat">📡 ${observers.length} Total</span>
|
||||
</div>
|
||||
<table class="data-table obs-table" id="obsTable">
|
||||
<caption class="sr-only">Observer status and statistics</caption>
|
||||
<thead><tr>
|
||||
<th>Status</th><th>Name</th><th>Region</th><th>Last Seen</th>
|
||||
<th>Packets</th><th>Packets/Hour</th><th>Uptime</th>
|
||||
|
||||
+2
-1
@@ -672,7 +672,8 @@ button.ch-item.selected { background: var(--selected-bg); }
|
||||
.health-dot.health-yellow { background: #eab308; box-shadow: 0 0 6px #eab30880; }
|
||||
.health-dot.health-red { background: #ef4444; box-shadow: 0 0 6px #ef444480; }
|
||||
.obs-table td:first-child { white-space: nowrap; }
|
||||
.spark-bar { position: relative; width: 100px; height: 18px; background: var(--border); border-radius: 4px; overflow: hidden; display: inline-block; vertical-align: middle; }
|
||||
.spark-bar { position: relative; min-width: 60px; max-width: 100px; flex: 1; height: 18px; background: var(--border); border-radius: 4px; overflow: hidden; display: inline-block; vertical-align: middle; }
|
||||
@media (max-width: 640px) { .spark-bar { max-width: 60px; } }
|
||||
.spark-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #60a5fa); border-radius: 4px; transition: width 0.3s; }
|
||||
.spark-label { position: absolute; right: 4px; top: 0; line-height: 18px; font-size: 11px; color: var(--text); font-weight: 500; }
|
||||
|
||||
|
||||
@@ -1250,7 +1250,7 @@ app.get('/api/observers', (req, res) => {
|
||||
const lastHour = db.db.prepare(`SELECT COUNT(*) as count FROM packets WHERE observer_id = ? AND timestamp > ?`).get(o.id, oneHourAgo);
|
||||
return { ...o, packetsLastHour: lastHour.count };
|
||||
});
|
||||
res.json({ observers: result });
|
||||
res.json({ observers: result, server_time: new Date().toISOString() });
|
||||
});
|
||||
|
||||
app.get('/api/traces/:hash', (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user