fix: get meshcore protocol icon displaying correctly (#681)

This commit is contained in:
Ben Allfree
2026-04-04 04:00:25 -07:00
committed by GitHub
parent 257e26c996
commit 7e0ba60a22
6 changed files with 78 additions and 65 deletions
@@ -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');
});
@@ -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', () => {
+45 -13
View File
@@ -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(`<strong>${longNameLink}</strong>`);
} 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),
+3 -45
View File
@@ -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 ? `<span class="node-detail__identifier">[${escapeHtml(identifier)}]</span>` : '';
const iconPrefix = isMeshtasticProtocol(nodeProtocol) ? `${meshtasticIconHtml()} ` : '';
const iconPrefix = protocolIconPrefixHtml(nodeProtocol);
const nameHtml = longName ? `<span class="node-detail__name">${iconPrefix}${escapeHtml(longName)}</span>` : '';
const badgeHtml = `<span class="node-detail__badge">${roleAwareBadge}</span>`;
const tableSection = tableHtml ? `<div class="node-detail__table">${tableHtml}</div>` : '';
+5 -4
View File
@@ -27,7 +27,7 @@
*/
import { escapeHtml } from './utils.js';
import { isMeshtasticProtocol, meshtasticIconHtml } from './protocol-helpers.js';
import { protocolIconPrefixHtml } from './protocol-helpers.js';
/**
* Normalise node name fields by trimming whitespace.
@@ -79,8 +79,9 @@ export function canonicalNodeIdentifier(identifier) {
/**
* Render a linked long name pointing to the node detail view.
*
* When ``protocol`` is ``"meshtastic"`` (or absent) the Meshtastic logo is
* prepended to the displayed name. An anchor element is only emitted when
* When ``protocol`` is Meshtastic (including null/empty per
* {@link module:protocol-helpers~isMeshtasticProtocol}) or ``"meshcore"``, the
* matching protocol icon is prepended. An anchor element is only emitted when
* ``identifier`` resolves to a non-null detail path.
*
* @param {string|null} longName Display name.
@@ -91,7 +92,7 @@ export function canonicalNodeIdentifier(identifier) {
export function renderNodeLongNameLink(longName, identifier, { className = 'node-long-link', protocol = null } = {}) {
const text = normalizeNodeNameValue(longName);
if (!text) return '';
const iconPrefix = isMeshtasticProtocol(protocol) ? `${meshtasticIconHtml()} ` : '';
const iconPrefix = protocolIconPrefixHtml(protocol);
const href = buildNodeDetailHref(identifier);
if (!href) {
return `${iconPrefix}${escapeHtml(text)}`;
@@ -80,3 +80,24 @@ export function meshcoreIconHtml() {
' aria-hidden="true">';
}
/**
* Build an HTML prefix (protocol icon plus a trailing space) for inline UI.
*
* Meshtastic including null, undefined, empty, or whitespace-only values per
* {@link isMeshtasticProtocol} uses the Meshtastic icon. The literal
* ``"meshcore"`` uses the MeshCore icon. Any other protocol string yields an
* empty prefix (same as the pre-MeshCore behaviour for unknown stacks).
*
* @param {string|null|undefined} protocol Protocol string from the API.
* @returns {string} HTML fragment safe to concatenate before visible text.
*/
export function protocolIconPrefixHtml(protocol) {
if (isMeshcoreProtocol(protocol)) {
return `${meshcoreIconHtml()} `;
}
if (isMeshtasticProtocol(protocol)) {
return `${meshtasticIconHtml()} `;
}
return '';
}