mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-28 09:25:16 +00:00
feat: node aging M1 — visual aging on map + list
Two-state node freshness: Active vs Stale - roles.js: add getNodeStatus(role, lastSeenMs) helper returning 'active'/'stale' - Repeaters/Rooms: stale after 72h - Companions/Sensors: stale after 24h - Backward compat: getHealthThresholds() with degradedMs/silentMs still works - map.js: stale markers get .marker-stale CSS class (opacity 0.35, grayscale 70%) - Applied to both SVG shape markers and hash label markers - makeMarkerIcon() and makeRepeaterLabelIcon() accept isStale parameter - nodes.js: visual aging in table, side pane, and full detail - Table: Last Seen column colored green (active) or muted (stale) - Side pane: status shows 🟢 Active or ⚪ Stale (was 🟢/🟡/🔴) - Full detail: Status row with role-appropriate explanation - Stale repeaters: 'not heard for Xd — repeaters typically advertise every 12-24h' - Stale companions: 'companions only advertise when user initiates' - Fixed lastHeard fallback to n.last_seen when health API has no stats - style.css: .marker-stale, .last-seen-active, .last-seen-stale classes
This commit is contained in:
+6
-5
@@ -20,7 +20,7 @@
|
||||
|
||||
// Roles loaded from shared roles.js (ROLE_STYLE, ROLE_LABELS, ROLE_COLORS globals)
|
||||
|
||||
function makeMarkerIcon(role) {
|
||||
function makeMarkerIcon(role, isStale) {
|
||||
const s = ROLE_STYLE[role] || ROLE_STYLE.companion;
|
||||
const size = s.radius * 2 + 4;
|
||||
const c = size / 2;
|
||||
@@ -54,14 +54,14 @@
|
||||
const svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">${path}</svg>`;
|
||||
return L.divIcon({
|
||||
html: svg,
|
||||
className: 'meshcore-marker',
|
||||
className: 'meshcore-marker' + (isStale ? ' marker-stale' : ''),
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [c, c],
|
||||
popupAnchor: [0, -c],
|
||||
});
|
||||
}
|
||||
|
||||
function makeRepeaterLabelIcon(node) {
|
||||
function makeRepeaterLabelIcon(node, isStale) {
|
||||
var s = ROLE_STYLE['repeater'] || ROLE_STYLE.companion;
|
||||
var hs = node.hash_size || 1;
|
||||
// Show the short mesh hash ID (first N bytes of pubkey, uppercased)
|
||||
@@ -71,7 +71,7 @@
|
||||
shortHash + '</div>';
|
||||
return L.divIcon({
|
||||
html: html,
|
||||
className: 'meshcore-marker meshcore-label-marker',
|
||||
className: 'meshcore-marker meshcore-label-marker' + (isStale ? ' marker-stale' : ''),
|
||||
iconSize: null,
|
||||
iconAnchor: [14, 12],
|
||||
popupAnchor: [0, -12],
|
||||
@@ -545,8 +545,9 @@
|
||||
const allMarkers = [];
|
||||
|
||||
for (const node of filtered) {
|
||||
const isStale = getNodeStatus(node.role || 'companion', node.last_seen ? new Date(node.last_seen).getTime() : 0) === 'stale';
|
||||
const useLabel = node.role === 'repeater' && filters.hashLabels;
|
||||
const icon = useLabel ? makeRepeaterLabelIcon(node) : makeMarkerIcon(node.role || 'companion');
|
||||
const icon = useLabel ? makeRepeaterLabelIcon(node, isStale) : makeMarkerIcon(node.role || 'companion', isStale);
|
||||
const latLng = L.latLng(node.lat, node.lon);
|
||||
allMarkers.push({ latLng, node, icon, isLabel: useLabel, popupFn: function() { return buildPopup(node); }, alt: (node.name || 'Unknown') + ' (' + (node.role || 'node') + ')' });
|
||||
}
|
||||
|
||||
+4
-2
@@ -163,7 +163,8 @@
|
||||
// Repeaters/rooms: flood advert every 12-24h, so degraded after 24h, silent after 72h
|
||||
// Companions/sensors: user-initiated adverts, shorter thresholds
|
||||
const role = (n.role || '').toLowerCase();
|
||||
const lastHeardMs = lastHeard ? new Date(lastHeard).getTime() : 0;
|
||||
const lastHeardTime = lastHeard || n.last_seen;
|
||||
const lastHeardMs = lastHeardTime ? new Date(lastHeardTime).getTime() : 0;
|
||||
const status = getNodeStatus(role, lastHeardMs);
|
||||
const statusLabel = status === 'active' ? '🟢 Active' : '⚪ Stale';
|
||||
const statusAge = lastHeardMs ? (Date.now() - lastHeardMs) : Infinity;
|
||||
@@ -577,7 +578,8 @@
|
||||
|
||||
// Status calculation
|
||||
const lastHeard = stats.lastHeard;
|
||||
const lastHeardMs = lastHeard ? new Date(lastHeard).getTime() : 0;
|
||||
const lastHeardTime = lastHeard || n.last_seen;
|
||||
const lastHeardMs = lastHeardTime ? new Date(lastHeardTime).getTime() : 0;
|
||||
const role = (n.role || '').toLowerCase();
|
||||
const status = getNodeStatus(role, lastHeardMs);
|
||||
const statusLabel = status === 'active' ? '🟢 Active' : '⚪ Stale';
|
||||
|
||||
+9
-1
@@ -75,7 +75,7 @@
|
||||
nodeSilentMs: 86400000 // 24h
|
||||
};
|
||||
|
||||
// Helper: get degraded/silent thresholds for a role
|
||||
// Helper: get degraded/silent thresholds for a role (backward compat)
|
||||
window.getHealthThresholds = function (role) {
|
||||
var isInfra = role === 'repeater' || role === 'room';
|
||||
return {
|
||||
@@ -84,6 +84,14 @@
|
||||
};
|
||||
};
|
||||
|
||||
// Simplified two-state helper: returns 'active' or 'stale'
|
||||
window.getNodeStatus = function (role, lastSeenMs) {
|
||||
var isInfra = role === 'repeater' || role === 'room';
|
||||
var staleMs = isInfra ? HEALTH_THRESHOLDS.infraSilentMs : HEALTH_THRESHOLDS.nodeSilentMs;
|
||||
var age = typeof lastSeenMs === 'number' ? (Date.now() - lastSeenMs) : Infinity;
|
||||
return age < staleMs ? 'active' : 'stale';
|
||||
};
|
||||
|
||||
// ─── Tile URLs ───
|
||||
window.TILE_DARK = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
|
||||
window.TILE_LIGHT = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
|
||||
Reference in New Issue
Block a user