mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-01 01:05:39 +00:00
Issues fixed: - #127: Firefox copy URL - shared copyToClipboard() with execCommand fallback - #125: Dismiss packet detail pane - close button with keyboard support - #124: Customize window scrollbar - flex layout fix for overflow - #122: Last Activity stale times - use last_heard || last_seen Test improvements: - E2E perf: replace 19 networkidle waits, cut navigations 14->7, remove 11 sleeps - 8 new unit tests for copyToClipboard helper (47->55 in test-frontend-helpers) - 1 new E2E test for packet pane dismiss Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
398 lines
13 KiB
JavaScript
398 lines
13 KiB
JavaScript
/* === MeshCore Analyzer — roles.js (shared config module) === */
|
|
'use strict';
|
|
|
|
/*
|
|
* Centralized roles, thresholds, tile URLs, and UI constants.
|
|
* Loaded BEFORE all page scripts via index.html.
|
|
* Defaults are set synchronously; server config overrides arrive via fetch.
|
|
*/
|
|
|
|
(function () {
|
|
// ─── Role definitions ───
|
|
window.ROLE_COLORS = {
|
|
repeater: '#dc2626', companion: '#2563eb', room: '#16a34a',
|
|
sensor: '#d97706', observer: '#8b5cf6', unknown: '#6b7280'
|
|
};
|
|
|
|
window.TYPE_COLORS = {
|
|
ADVERT: '#22c55e', GRP_TXT: '#3b82f6', TXT_MSG: '#f59e0b', ACK: '#6b7280',
|
|
REQUEST: '#a855f7', RESPONSE: '#06b6d4', TRACE: '#ec4899', PATH: '#14b8a6',
|
|
ANON_REQ: '#f43f5e', UNKNOWN: '#6b7280'
|
|
};
|
|
|
|
// Badge CSS class name mapping
|
|
const TYPE_BADGE_MAP = {
|
|
ADVERT: 'advert', GRP_TXT: 'grp-txt', TXT_MSG: 'txt-msg', ACK: 'ack',
|
|
REQUEST: 'req', RESPONSE: 'response', TRACE: 'trace', PATH: 'path',
|
|
ANON_REQ: 'anon-req', UNKNOWN: 'unknown'
|
|
};
|
|
|
|
// Generate badge CSS from TYPE_COLORS — single source of truth
|
|
window.syncBadgeColors = function() {
|
|
var el = document.getElementById('type-color-badges');
|
|
if (!el) { el = document.createElement('style'); el.id = 'type-color-badges'; document.head.appendChild(el); }
|
|
var css = '';
|
|
for (var type in TYPE_BADGE_MAP) {
|
|
var color = window.TYPE_COLORS[type];
|
|
if (!color) continue;
|
|
var cls = TYPE_BADGE_MAP[type];
|
|
css += '.badge-' + cls + ' { background: ' + color + '20; color: ' + color + '; }\n';
|
|
}
|
|
el.textContent = css;
|
|
};
|
|
|
|
// Auto-sync on load
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', window.syncBadgeColors);
|
|
} else {
|
|
window.syncBadgeColors();
|
|
}
|
|
|
|
window.ROLE_LABELS = {
|
|
repeater: 'Repeaters', companion: 'Companions', room: 'Room Servers',
|
|
sensor: 'Sensors', observer: 'Observers'
|
|
};
|
|
|
|
window.ROLE_STYLE = {
|
|
repeater: { color: '#dc2626', shape: 'diamond', radius: 10, weight: 2 },
|
|
companion: { color: '#2563eb', shape: 'circle', radius: 8, weight: 2 },
|
|
room: { color: '#16a34a', shape: 'square', radius: 9, weight: 2 },
|
|
sensor: { color: '#d97706', shape: 'triangle', radius: 8, weight: 2 },
|
|
observer: { color: '#8b5cf6', shape: 'star', radius: 11, weight: 2 }
|
|
};
|
|
|
|
window.ROLE_EMOJI = {
|
|
repeater: '◆', companion: '●', room: '■', sensor: '▲', observer: '★'
|
|
};
|
|
|
|
window.ROLE_SORT = ['repeater', 'companion', 'room', 'sensor', 'observer'];
|
|
|
|
// ─── Health thresholds (ms) ───
|
|
window.HEALTH_THRESHOLDS = {
|
|
infraDegradedMs: 86400000, // 24h
|
|
infraSilentMs: 259200000, // 72h
|
|
nodeDegradedMs: 3600000, // 1h
|
|
nodeSilentMs: 86400000 // 24h
|
|
};
|
|
|
|
// Helper: get degraded/silent thresholds for a role (backward compat)
|
|
window.getHealthThresholds = function (role) {
|
|
var isInfra = role === 'repeater' || role === 'room';
|
|
return {
|
|
degradedMs: isInfra ? HEALTH_THRESHOLDS.infraDegradedMs : HEALTH_THRESHOLDS.nodeDegradedMs,
|
|
silentMs: isInfra ? HEALTH_THRESHOLDS.infraSilentMs : HEALTH_THRESHOLDS.nodeSilentMs
|
|
};
|
|
};
|
|
|
|
// 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';
|
|
|
|
window.getTileUrl = function () {
|
|
var isDark = document.documentElement.getAttribute('data-theme') === 'dark' ||
|
|
(document.documentElement.getAttribute('data-theme') !== 'light' &&
|
|
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
return isDark ? TILE_DARK : TILE_LIGHT;
|
|
};
|
|
|
|
// ─── SNR thresholds ───
|
|
window.SNR_THRESHOLDS = { excellent: 6, good: 0 };
|
|
|
|
// ─── Distance thresholds (km) ───
|
|
window.DIST_THRESHOLDS = { local: 50, regional: 200 };
|
|
|
|
// ─── MAX_HOP_DIST (degrees, ~200km ≈ 1.8°) ───
|
|
window.MAX_HOP_DIST = 1.8;
|
|
|
|
// ─── Result limits ───
|
|
window.LIMITS = {
|
|
topNodes: 15,
|
|
topPairs: 12,
|
|
topRingNodes: 8,
|
|
topSenders: 10,
|
|
topCollisionNodes: 10,
|
|
recentReplay: 8,
|
|
feedMax: 25
|
|
};
|
|
|
|
// ─── Performance thresholds ───
|
|
window.PERF_SLOW_MS = 100;
|
|
|
|
// ─── WebSocket reconnect delay (ms) ───
|
|
window.WS_RECONNECT_MS = 3000;
|
|
|
|
// ─── Propagation buffer (ms) for realistic mode ───
|
|
window.PROPAGATION_BUFFER_MS = 5000;
|
|
|
|
// ─── Cache invalidation debounce (ms) ───
|
|
window.CACHE_INVALIDATE_MS = 5000;
|
|
|
|
// ─── External URLs ───
|
|
window.EXTERNAL_URLS = {
|
|
flasher: 'https://flasher.meshcore.co.uk/'
|
|
};
|
|
|
|
// ─── Fetch server overrides ───
|
|
window.MeshConfigReady = fetch('/api/config/client').then(function (r) { return r.json(); }).then(function (cfg) {
|
|
if (cfg.roles) {
|
|
if (cfg.roles.colors) Object.assign(ROLE_COLORS, cfg.roles.colors);
|
|
if (cfg.roles.labels) Object.assign(ROLE_LABELS, cfg.roles.labels);
|
|
if (cfg.roles.style) {
|
|
for (var k in cfg.roles.style) ROLE_STYLE[k] = Object.assign(ROLE_STYLE[k] || {}, cfg.roles.style[k]);
|
|
}
|
|
if (cfg.roles.emoji) Object.assign(ROLE_EMOJI, cfg.roles.emoji);
|
|
if (cfg.roles.sort) window.ROLE_SORT = cfg.roles.sort;
|
|
}
|
|
if (cfg.healthThresholds) Object.assign(HEALTH_THRESHOLDS, cfg.healthThresholds);
|
|
if (cfg.tiles) {
|
|
if (cfg.tiles.dark) window.TILE_DARK = cfg.tiles.dark;
|
|
if (cfg.tiles.light) window.TILE_LIGHT = cfg.tiles.light;
|
|
}
|
|
if (cfg.snrThresholds) Object.assign(SNR_THRESHOLDS, cfg.snrThresholds);
|
|
if (cfg.distThresholds) Object.assign(DIST_THRESHOLDS, cfg.distThresholds);
|
|
if (cfg.maxHopDist != null) window.MAX_HOP_DIST = cfg.maxHopDist;
|
|
if (cfg.limits) Object.assign(LIMITS, cfg.limits);
|
|
if (cfg.perfSlowMs != null) window.PERF_SLOW_MS = cfg.perfSlowMs;
|
|
if (cfg.wsReconnectMs != null) window.WS_RECONNECT_MS = cfg.wsReconnectMs;
|
|
if (cfg.cacheInvalidateMs != null) window.CACHE_INVALIDATE_MS = cfg.cacheInvalidateMs;
|
|
if (cfg.externalUrls) Object.assign(EXTERNAL_URLS, cfg.externalUrls);
|
|
if (cfg.propagationBufferMs != null) window.PROPAGATION_BUFFER_MS = cfg.propagationBufferMs;
|
|
// Sync ROLE_STYLE colors with ROLE_COLORS
|
|
for (var role in ROLE_STYLE) {
|
|
if (ROLE_COLORS[role]) ROLE_STYLE[role].color = ROLE_COLORS[role];
|
|
}
|
|
}).catch(function () { /* use defaults */ });
|
|
|
|
// ─── Built-in IATA airport code → city name mapping ───
|
|
window.IATA_CITIES = {
|
|
// United States
|
|
'SEA': 'Seattle, WA',
|
|
'SFO': 'San Francisco, CA',
|
|
'PDX': 'Portland, OR',
|
|
'LAX': 'Los Angeles, CA',
|
|
'DEN': 'Denver, CO',
|
|
'SLC': 'Salt Lake City, UT',
|
|
'PHX': 'Phoenix, AZ',
|
|
'DFW': 'Dallas, TX',
|
|
'ATL': 'Atlanta, GA',
|
|
'ORD': 'Chicago, IL',
|
|
'JFK': 'New York, NY',
|
|
'LGA': 'New York, NY',
|
|
'BOS': 'Boston, MA',
|
|
'MIA': 'Miami, FL',
|
|
'FLL': 'Fort Lauderdale, FL',
|
|
'IAH': 'Houston, TX',
|
|
'HOU': 'Houston, TX',
|
|
'MSP': 'Minneapolis, MN',
|
|
'DTW': 'Detroit, MI',
|
|
'CLT': 'Charlotte, NC',
|
|
'EWR': 'Newark, NJ',
|
|
'IAD': 'Washington, DC',
|
|
'DCA': 'Washington, DC',
|
|
'BWI': 'Baltimore, MD',
|
|
'LAS': 'Las Vegas, NV',
|
|
'MCO': 'Orlando, FL',
|
|
'TPA': 'Tampa, FL',
|
|
'BNA': 'Nashville, TN',
|
|
'AUS': 'Austin, TX',
|
|
'SAT': 'San Antonio, TX',
|
|
'RDU': 'Raleigh, NC',
|
|
'SAN': 'San Diego, CA',
|
|
'OAK': 'Oakland, CA',
|
|
'SJC': 'San Jose, CA',
|
|
'SMF': 'Sacramento, CA',
|
|
'PHL': 'Philadelphia, PA',
|
|
'PIT': 'Pittsburgh, PA',
|
|
'CLE': 'Cleveland, OH',
|
|
'CMH': 'Columbus, OH',
|
|
'CVG': 'Cincinnati, OH',
|
|
'IND': 'Indianapolis, IN',
|
|
'MCI': 'Kansas City, MO',
|
|
'STL': 'St. Louis, MO',
|
|
'MSY': 'New Orleans, LA',
|
|
'MEM': 'Memphis, TN',
|
|
'SDF': 'Louisville, KY',
|
|
'JAX': 'Jacksonville, FL',
|
|
'RIC': 'Richmond, VA',
|
|
'ORF': 'Norfolk, VA',
|
|
'BDL': 'Hartford, CT',
|
|
'PVD': 'Providence, RI',
|
|
'ABQ': 'Albuquerque, NM',
|
|
'OKC': 'Oklahoma City, OK',
|
|
'TUL': 'Tulsa, OK',
|
|
'OMA': 'Omaha, NE',
|
|
'BOI': 'Boise, ID',
|
|
'GEG': 'Spokane, WA',
|
|
'ANC': 'Anchorage, AK',
|
|
'HNL': 'Honolulu, HI',
|
|
'OGG': 'Maui, HI',
|
|
'BUF': 'Buffalo, NY',
|
|
'SYR': 'Syracuse, NY',
|
|
'ROC': 'Rochester, NY',
|
|
'ALB': 'Albany, NY',
|
|
'BTV': 'Burlington, VT',
|
|
'PWM': 'Portland, ME',
|
|
'MKE': 'Milwaukee, WI',
|
|
'DSM': 'Des Moines, IA',
|
|
'LIT': 'Little Rock, AR',
|
|
'BHM': 'Birmingham, AL',
|
|
'CHS': 'Charleston, SC',
|
|
'SAV': 'Savannah, GA',
|
|
// Canada
|
|
'YVR': 'Vancouver, BC',
|
|
'YYZ': 'Toronto, ON',
|
|
'YUL': 'Montreal, QC',
|
|
'YOW': 'Ottawa, ON',
|
|
'YYC': 'Calgary, AB',
|
|
'YEG': 'Edmonton, AB',
|
|
'YWG': 'Winnipeg, MB',
|
|
'YHZ': 'Halifax, NS',
|
|
'YQB': 'Quebec City, QC',
|
|
// Europe
|
|
'LHR': 'London, UK',
|
|
'LGW': 'London, UK',
|
|
'STN': 'London, UK',
|
|
'CDG': 'Paris, FR',
|
|
'ORY': 'Paris, FR',
|
|
'FRA': 'Frankfurt, DE',
|
|
'MUC': 'Munich, DE',
|
|
'BER': 'Berlin, DE',
|
|
'AMS': 'Amsterdam, NL',
|
|
'MAD': 'Madrid, ES',
|
|
'BCN': 'Barcelona, ES',
|
|
'FCO': 'Rome, IT',
|
|
'MXP': 'Milan, IT',
|
|
'ZRH': 'Zurich, CH',
|
|
'GVA': 'Geneva, CH',
|
|
'VIE': 'Vienna, AT',
|
|
'CPH': 'Copenhagen, DK',
|
|
'ARN': 'Stockholm, SE',
|
|
'OSL': 'Oslo, NO',
|
|
'HEL': 'Helsinki, FI',
|
|
'DUB': 'Dublin, IE',
|
|
'LIS': 'Lisbon, PT',
|
|
'ATH': 'Athens, GR',
|
|
'IST': 'Istanbul, TR',
|
|
'WAW': 'Warsaw, PL',
|
|
'PRG': 'Prague, CZ',
|
|
'BUD': 'Budapest, HU',
|
|
'OTP': 'Bucharest, RO',
|
|
'SOF': 'Sofia, BG',
|
|
'ZAG': 'Zagreb, HR',
|
|
'BEG': 'Belgrade, RS',
|
|
'KBP': 'Kyiv, UA',
|
|
'LED': 'St. Petersburg, RU',
|
|
'SVO': 'Moscow, RU',
|
|
'BRU': 'Brussels, BE',
|
|
'EDI': 'Edinburgh, UK',
|
|
'MAN': 'Manchester, UK',
|
|
// Asia
|
|
'NRT': 'Tokyo, JP',
|
|
'HND': 'Tokyo, JP',
|
|
'KIX': 'Osaka, JP',
|
|
'ICN': 'Seoul, KR',
|
|
'PEK': 'Beijing, CN',
|
|
'PVG': 'Shanghai, CN',
|
|
'HKG': 'Hong Kong',
|
|
'TPE': 'Taipei, TW',
|
|
'SIN': 'Singapore',
|
|
'BKK': 'Bangkok, TH',
|
|
'KUL': 'Kuala Lumpur, MY',
|
|
'CGK': 'Jakarta, ID',
|
|
'MNL': 'Manila, PH',
|
|
'DEL': 'New Delhi, IN',
|
|
'BOM': 'Mumbai, IN',
|
|
'BLR': 'Bangalore, IN',
|
|
'CCU': 'Kolkata, IN',
|
|
'SGN': 'Ho Chi Minh City, VN',
|
|
'HAN': 'Hanoi, VN',
|
|
'DOH': 'Doha, QA',
|
|
'DXB': 'Dubai, AE',
|
|
'AUH': 'Abu Dhabi, AE',
|
|
'TLV': 'Tel Aviv, IL',
|
|
// Oceania
|
|
'SYD': 'Sydney, AU',
|
|
'MEL': 'Melbourne, AU',
|
|
'BNE': 'Brisbane, AU',
|
|
'PER': 'Perth, AU',
|
|
'AKL': 'Auckland, NZ',
|
|
'WLG': 'Wellington, NZ',
|
|
'CHC': 'Christchurch, NZ',
|
|
// South America
|
|
'GRU': 'São Paulo, BR',
|
|
'GIG': 'Rio de Janeiro, BR',
|
|
'EZE': 'Buenos Aires, AR',
|
|
'SCL': 'Santiago, CL',
|
|
'BOG': 'Bogota, CO',
|
|
'LIM': 'Lima, PE',
|
|
'UIO': 'Quito, EC',
|
|
'CCS': 'Caracas, VE',
|
|
'MVD': 'Montevideo, UY',
|
|
// Africa
|
|
'JNB': 'Johannesburg, ZA',
|
|
'CPT': 'Cape Town, ZA',
|
|
'CAI': 'Cairo, EG',
|
|
'NBO': 'Nairobi, KE',
|
|
'ADD': 'Addis Ababa, ET',
|
|
'CMN': 'Casablanca, MA',
|
|
'LOS': 'Lagos, NG'
|
|
};
|
|
|
|
// Copy text to clipboard with fallback for Firefox and older browsers
|
|
window.copyToClipboard = function(text, onSuccess, onFail) {
|
|
function fallback() {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed';
|
|
ta.style.left = '-9999px';
|
|
ta.style.opacity = '0';
|
|
document.body.appendChild(ta);
|
|
ta.focus();
|
|
ta.select();
|
|
try {
|
|
var ok = document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
if (ok && onSuccess) onSuccess();
|
|
else if (!ok && onFail) onFail();
|
|
} catch (e) {
|
|
document.body.removeChild(ta);
|
|
if (onFail) onFail();
|
|
}
|
|
}
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(text).then(
|
|
function() { if (onSuccess) onSuccess(); },
|
|
function() { fallback(); }
|
|
);
|
|
} else {
|
|
fallback();
|
|
}
|
|
};
|
|
|
|
// Simple markdown → HTML (bold, italic, links, code, lists, line breaks)
|
|
window.miniMarkdown = function(text) {
|
|
if (!text) return '';
|
|
var html = text
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener" style="color:var(--accent)">$1</a>')
|
|
.replace(/^- (.+)/gm, '<li>$1</li>')
|
|
.replace(/\n/g, '<br>');
|
|
// Wrap consecutive <li> in <ul>
|
|
html = html.replace(/((?:<li>.*?<\/li><br>?)+)/g, function(m) {
|
|
return '<ul>' + m.replace(/<br>/g, '') + '</ul>';
|
|
});
|
|
return html;
|
|
};
|
|
})();
|