diff --git a/web/public/assets/js/app/__tests__/node-page-charts.test.js b/web/public/assets/js/app/__tests__/node-page-charts.test.js index 9108101..4a4d665 100644 --- a/web/public/assets/js/app/__tests__/node-page-charts.test.js +++ b/web/public/assets/js/app/__tests__/node-page-charts.test.js @@ -144,8 +144,8 @@ test('padTwo handles zero', () => { // --------------------------------------------------------------------------- test('formatCompactDate returns two-digit day of month', () => { - // 2025-01-05 UTC - const ts = Date.UTC(2025, 0, 5); + // Local calendar date (formatCompactDate uses getDate(), not UTC). + const ts = new Date(2025, 0, 5).getTime(); assert.equal(formatCompactDate(ts), '05'); }); diff --git a/web/public/assets/js/app/__tests__/node-rendering.test.js b/web/public/assets/js/app/__tests__/node-rendering.test.js index ca635a5..97df3a5 100644 --- a/web/public/assets/js/app/__tests__/node-rendering.test.js +++ b/web/public/assets/js/app/__tests__/node-rendering.test.js @@ -132,9 +132,10 @@ test('renderNodeLongNameLink renders meshtastic icon when protocol is absent', ( assert.ok(html.includes('meshtastic.svg')); }); -test('renderNodeLongNameLink omits meshtastic icon for meshcore protocol', () => { +test('renderNodeLongNameLink uses meshcore icon for meshcore protocol', () => { const html = renderNodeLongNameLink('Eve', '!aabbccdd', { protocol: 'meshcore' }); assert.ok(!html.includes('meshtastic.svg'), 'no meshtastic icon for meshcore protocol'); + assert.ok(html.includes('meshcore.svg'), 'meshcore icon should be shown'); }); test('renderNodeLongNameLink renders plain text when identifier is null', () => { diff --git a/web/public/assets/js/app/main.js b/web/public/assets/js/app/main.js index ab84f35..02951d5 100644 --- a/web/public/assets/js/app/main.js +++ b/web/public/assets/js/app/main.js @@ -99,7 +99,14 @@ import { roleColors, roleRenderOrder, } from './role-helpers.js'; -import { isMeshtasticProtocol, meshtasticIconHtml, MESHTASTIC_ICON_SRC } from './protocol-helpers.js'; +import { + isMeshtasticProtocol, + isMeshcoreProtocol, + meshtasticIconHtml, + MESHTASTIC_ICON_SRC, + MESHCORE_ICON_SRC, + protocolIconPrefixHtml, +} from './protocol-helpers.js'; /** * Entry point for the interactive dashboard. Wires up event listeners, @@ -2247,7 +2254,9 @@ export function initializeApp(config) { overlayInfo.role = 'CLIENT'; } const lines = []; - const longNameLink = renderNodeLongNameLink(overlayInfo.longName, overlayInfo.nodeId); + const longNameLink = renderNodeLongNameLink(overlayInfo.longName, overlayInfo.nodeId, { + protocol: overlayInfo.protocol, + }); if (longNameLink) { lines.push(`${longNameLink}`); } else { @@ -2382,7 +2391,8 @@ export function initializeApp(config) { const fallbackId = nodeIdRaw || 'Unknown node'; const longNameRaw = pickFirstProperty([node], ['long_name', 'longName']); const longNameDisplay = longNameRaw ? String(longNameRaw) : fallbackId; - const longNameLink = renderNodeLongNameLink(longNameRaw, nodeIdRaw); + const nodeProtocol = pickFirstProperty([node], ['protocol']); + const longNameLink = renderNodeLongNameLink(longNameRaw, nodeIdRaw, { protocol: nodeProtocol }); const announcementName = longNameLink || escapeHtml(longNameDisplay); const shortNameRaw = pickFirstProperty([node], ['short_name', 'shortName']); const shortNameDisplay = shortNameRaw ? String(shortNameRaw) : (nodeIdRaw ? nodeIdRaw.slice(-4) : null); @@ -2397,6 +2407,7 @@ export function initializeApp(config) { role: roleDisplay, metadataSource: node, nodeData: node, + protocol: nodeProtocol, messageHtml: `${renderEmojiHtml('☀️')} ${renderAnnouncementCopy('New node:', ` ${announcementName}`)}` }); } @@ -2472,6 +2483,7 @@ export function initializeApp(config) { role: context.role, metadataSource: context.metadataSource, nodeData: context.nodeData, + protocol: context.protocol, messageHtml: `${renderEmojiHtml('💾')} ${renderAnnouncementCopy('Updated node info')}` }); } @@ -2486,6 +2498,7 @@ export function initializeApp(config) { role: context.role, metadataSource: context.metadataSource, nodeData: context.nodeData, + protocol: context.protocol, messageHtml: `${renderEmojiHtml('🔋')} ${renderAnnouncementCopy('Broadcasted telemetry', highlightSuffix)}` }); } @@ -2500,6 +2513,7 @@ export function initializeApp(config) { role: context.role, metadataSource: context.metadataSource, nodeData: context.nodeData, + protocol: context.protocol, messageHtml: `${renderEmojiHtml('📍')} ${renderAnnouncementCopy('Broadcasted position info', highlightSuffix)}` }); } @@ -2525,6 +2539,7 @@ export function initializeApp(config) { role: context.role, metadataSource: context.metadataSource, nodeData: context.nodeData, + protocol: context.protocol, messageHtml: `${renderEmojiHtml('🏘️')} ${renderAnnouncementCopy('Broadcasted neighbor info', detail)}` }); } @@ -2563,7 +2578,8 @@ export function initializeApp(config) { * role: ?string, * metadataSource: Object|null, * nodeData: Object|null, - * messageHtml: string + * messageHtml: string, + * protocol: ?string * }} params Rendering parameters. * @returns {HTMLElement} Chat log element. */ @@ -2574,7 +2590,8 @@ export function initializeApp(config) { role, metadataSource, nodeData, - messageHtml + messageHtml, + protocol: protocolHint = null }) { const div = document.createElement('div'); const tsDate = timestampSeconds != null ? new Date(timestampSeconds * 1000) : null; @@ -2587,8 +2604,9 @@ export function initializeApp(config) { const presetTag = formatChatPresetTag({ presetCode: metadata.presetCode }); const longNameDisplay = longName != null ? String(longName) : ''; const shortHtml = renderShortHtml(shortName, role, longNameDisplay, nodeData || metadataSource || {}); - const announcementProtocol = (nodeData || metadataSource || {}).protocol; - const announcementIconPrefix = isMeshtasticProtocol(announcementProtocol) ? `${meshtasticIconHtml()} ` : ''; + const announcementProtocol = + protocolHint ?? pickFirstProperty([nodeData, metadataSource], ['protocol']); + const announcementIconPrefix = protocolIconPrefixHtml(announcementProtocol); div.className = 'chat-entry-node'; div.innerHTML = `${prefix}${presetTag} ${announcementIconPrefix}${shortHtml} ${messageHtml}`; return div; @@ -2632,6 +2650,7 @@ export function initializeApp(config) { role: context.role, metadataSource: sourceNode || context.metadataSource, nodeData: sourceNode || context.nodeData, + protocol: context.protocol, messageHtml: `${renderEmojiHtml('👣')} ${renderAnnouncementCopy('Caught trace', labelSuffix)}` }); } @@ -2737,13 +2756,20 @@ export function initializeApp(config) { * longName: ?string, * role: ?string, * metadataSource: Object|null, - * nodeData: Object|null + * nodeData: Object|null, + * protocol: ?string * }} Normalised display metadata. */ function buildDisplayContext(entry) { const resolvedNode = resolveNodeForLogEntry(entry); - const candidateSources = [resolvedNode, entry?.node, entry?.telemetry, entry?.position, entry?.neighbor] - .filter(source => source && typeof source === 'object'); + const candidateSources = [ + resolvedNode, + entry?.node, + entry?.telemetry, + entry?.position, + entry?.neighbor, + entry?.trace, + ].filter(source => source && typeof source === 'object'); const nodeId = typeof entry?.nodeId === 'string' && entry.nodeId.trim().length ? entry.nodeId.trim() : pickFirstProperty(candidateSources, ['node_id', 'nodeId']); @@ -2761,7 +2787,8 @@ export function initializeApp(config) { const role = pickFirstProperty(candidateSources, ['role']); const metadataSource = resolvedNode || candidateSources[0] || {}; const nodeData = resolvedNode || candidateSources[0] || {}; - return { nodeId, nodeNum, shortName, longName, role, metadataSource, nodeData }; + const protocol = pickFirstProperty(candidateSources, ['protocol']); + return { nodeId, nodeNum, shortName, longName, role, metadataSource, nodeData, protocol }; } /** @@ -2979,7 +3006,8 @@ export function initializeApp(config) { const tsDate = tsSeconds != null ? new Date(tsSeconds * 1000) : null; const ts = tsDate ? formatTime(tsDate) : '--:--:--'; const short = renderShortHtml(m.node?.short_name, m.node?.role, m.node?.long_name, m.node); - const nodeProtocolPrefix = isMeshtasticProtocol(m.node?.protocol) ? `${meshtasticIconHtml()} ` : ''; + const messageProtocol = pickFirstProperty([m, m?.node], ['protocol']); + const nodeProtocolPrefix = protocolIconPrefixHtml(messageProtocol); const replyPrefix = resolveReplyPrefix({ message: m, messagesById, @@ -3149,7 +3177,11 @@ export function initializeApp(config) { const channelTabs = filteredChannels.map(channel => ({ id: channel.id || `channel-${channel.index}`, label: channel.label, - iconSrc: isMeshtasticProtocol(channel.protocol) ? MESHTASTIC_ICON_SRC : null, + iconSrc: isMeshtasticProtocol(channel.protocol) + ? MESHTASTIC_ICON_SRC + : isMeshcoreProtocol(channel.protocol) + ? MESHCORE_ICON_SRC + : null, content: buildChatFragment({ entries: channel.entries.map(e => ({ ts: e.ts, item: e.message })), renderEntry: entry => createMessageChatEntry(entry.item), diff --git a/web/public/assets/js/app/node-page.js b/web/public/assets/js/app/node-page.js index 0d0e3fa..4128445 100644 --- a/web/public/assets/js/app/node-page.js +++ b/web/public/assets/js/app/node-page.js @@ -15,7 +15,7 @@ */ import { refreshNodeInformation } from './node-details.js'; -import { isMeshtasticProtocol, meshtasticIconHtml } from './protocol-helpers.js'; +import { protocolIconPrefixHtml } from './protocol-helpers.js'; import { extractChatMessageMetadata, formatChatChannelTag, @@ -114,48 +114,6 @@ function numberOrNull(value) { const num = Number(value); return Number.isFinite(num) ? num : null; } - - - - - - - - - - - - -/** - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Render the telemetry charts for the supplied node when telemetry snapshots @@ -1014,7 +972,7 @@ function renderMessages(messages, renderShortHtml, node) { const messageNode = message.node && typeof message.node === 'object' ? message.node : null; const messageProtocol = stringOrNull(messageNode?.protocol ?? fallbackNode?.protocol) ?? null; - const protocolIconHtml = isMeshtasticProtocol(messageProtocol) ? `${meshtasticIconHtml()} ` : ''; + const protocolIconHtml = protocolIconPrefixHtml(messageProtocol); const badgeHtml = renderRoleAwareBadge(renderShortHtml, { shortName: messageNode?.short_name ?? messageNode?.shortName ?? fallbackNode?.shortName ?? fallbackNode?.short_name, longName: messageNode?.long_name ?? messageNode?.longName ?? fallbackNode?.longName ?? fallbackNode?.long_name, @@ -1267,7 +1225,7 @@ function renderNodeDetailHtml(node, { } const identifierHtml = identifier ? `[${escapeHtml(identifier)}]` : ''; - const iconPrefix = isMeshtasticProtocol(nodeProtocol) ? `${meshtasticIconHtml()} ` : ''; + const iconPrefix = protocolIconPrefixHtml(nodeProtocol); const nameHtml = longName ? `${iconPrefix}${escapeHtml(longName)}` : ''; const badgeHtml = `${roleAwareBadge}`; const tableSection = tableHtml ? `