fix: observers — refresh a11y, table caption, spark ARIA, mobile, timezone (closes #93, #94, #95, #96, #97)

This commit is contained in:
you
2026-03-19 19:37:00 +00:00
parent a745f6cee0
commit afbaacaa7b
4 changed files with 20 additions and 9 deletions
+7 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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; }
+1 -1
View File
@@ -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) => {