Add preset mode to logs (#420)

This commit is contained in:
l5y
2025-11-07 17:56:27 +01:00
committed by GitHub
parent 487d618e00
commit 52486d82ad
3 changed files with 230 additions and 13 deletions
@@ -19,6 +19,7 @@ import {
extractChatMessageMetadata,
formatChatMessagePrefix,
formatChatChannelTag,
formatChatPresetTag,
formatNodeAnnouncementPrefix,
__test__
} from '../chat-format.js';
@@ -28,7 +29,13 @@ const {
normalizeString,
normalizeFrequency,
normalizeFrequencySlot,
FREQUENCY_PLACEHOLDER
FREQUENCY_PLACEHOLDER,
resolveModemPresetCandidate,
normalizePresetString,
abbreviatePreset,
derivePresetInitials,
normalizePresetSlot,
PRESET_PLACEHOLDER
} = __test__;
test('extractChatMessageMetadata prefers explicit region_frequency and channel_name', () => {
@@ -39,21 +46,32 @@ test('extractChatMessageMetadata prefers explicit region_frequency and channel_n
channelName: 'Ignored'
};
const result = extractChatMessageMetadata(payload);
assert.deepEqual(result, { frequency: '868', channelName: 'Test Channel' });
assert.deepEqual(result, { frequency: '868', channelName: 'Test Channel', presetCode: null });
});
test('extractChatMessageMetadata falls back to LoRa metadata', () => {
const payload = {
lora_freq: 915,
channelName: 'SpecChannel'
channelName: 'SpecChannel',
modem_preset: 'MediumFast'
};
const result = extractChatMessageMetadata(payload);
assert.deepEqual(result, { frequency: '915', channelName: 'SpecChannel' });
assert.deepEqual(result, { frequency: '915', channelName: 'SpecChannel', presetCode: 'MF' });
});
test('extractChatMessageMetadata returns null metadata for invalid input', () => {
assert.deepEqual(extractChatMessageMetadata(null), { frequency: null, channelName: null });
assert.deepEqual(extractChatMessageMetadata(undefined), { frequency: null, channelName: null });
assert.deepEqual(extractChatMessageMetadata(null), { frequency: null, channelName: null, presetCode: null });
assert.deepEqual(extractChatMessageMetadata(undefined), { frequency: null, channelName: null, presetCode: null });
});
test('extractChatMessageMetadata inspects nested node payloads for modem presets', () => {
const payload = {
node: {
modem_preset: 'ShortTurbo'
}
};
const result = extractChatMessageMetadata(payload);
assert.equal(result.presetCode, 'ST');
});
test('firstNonNull returns the first non-null candidate', () => {
@@ -107,6 +125,11 @@ test('formatChatChannelTag wraps channel names after the short name slot', () =>
);
});
test('formatChatPresetTag renders preset hints with placeholders', () => {
assert.equal(formatChatPresetTag({ presetCode: 'MF' }), '[MF]');
assert.equal(formatChatPresetTag({ presetCode: null }), `[${PRESET_PLACEHOLDER}]`);
});
test('formatNodeAnnouncementPrefix includes optional frequency bracket', () => {
assert.equal(
formatNodeAnnouncementPrefix({ timestamp: '12:34:56', frequency: '868' }),
@@ -124,3 +147,32 @@ test('normalizeFrequencySlot returns placeholder when frequency is missing', ()
assert.equal(normalizeFrequencySlot(undefined), FREQUENCY_PLACEHOLDER);
assert.equal(normalizeFrequencySlot('915'), '915');
});
test('resolveModemPresetCandidate walks nested payloads', () => {
const nested = { node: { modemPreset: 'LongFast' } };
assert.equal(resolveModemPresetCandidate(nested), 'LongFast');
});
test('normalizePresetString trims strings and ignores empties', () => {
assert.equal(normalizePresetString(' MediumSlow '), 'MediumSlow');
assert.equal(normalizePresetString(' '), null);
assert.equal(normalizePresetString(null), null);
});
test('abbreviatePreset maps known presets to codes', () => {
assert.equal(abbreviatePreset('VeryLongSlow'), 'VL');
assert.equal(abbreviatePreset('customPreset'), 'CP');
assert.equal(abbreviatePreset('X'), 'X?');
});
test('derivePresetInitials falls back to segmented tokens', () => {
assert.equal(derivePresetInitials('Long Moderate'), 'LM');
assert.equal(derivePresetInitials('ShortTurbo'), 'ST');
assert.equal(derivePresetInitials('Z'), 'Z?');
});
test('normalizePresetSlot enforces placeholders and uppercase output', () => {
assert.equal(normalizePresetSlot('mf'), 'MF');
assert.equal(normalizePresetSlot(''), PRESET_PLACEHOLDER);
assert.equal(normalizePresetSlot(null), PRESET_PLACEHOLDER);
});
+166 -4
View File
@@ -18,12 +18,12 @@
* Extract channel metadata from a message payload for chat display.
*
* @param {Object} message Raw message payload from the API.
* @returns {{ frequency: string|null, channelName: string|null }}
* @returns {{ frequency: string|null, channelName: string|null, presetCode: string|null }}
* Normalized metadata values.
*/
export function extractChatMessageMetadata(message) {
if (!message || typeof message !== 'object') {
return { frequency: null, channelName: null };
return { frequency: null, channelName: null, presetCode: null };
}
const frequency = normalizeFrequency(
@@ -40,7 +40,10 @@ export function extractChatMessageMetadata(message) {
firstNonNull(message.channel_name, message.channelName)
);
return { frequency, channelName };
const modemPreset = normalizePresetString(resolveModemPresetCandidate(message));
const presetCode = modemPreset ? abbreviatePreset(modemPreset) : null;
return { frequency, channelName, presetCode };
}
/**
@@ -92,6 +95,17 @@ export function formatNodeAnnouncementPrefix({ timestamp, frequency }) {
return `[${ts}][${freq}]`;
}
/**
* Render the preset hint bracket inserted between the prefix and short name.
*
* @param {{ presetCode: string|null }} params Normalized preset abbreviation.
* @returns {string} HTML-ready bracket slot.
*/
export function formatChatPresetTag({ presetCode }) {
const slot = normalizePresetSlot(presetCode);
return `[${slot}]`;
}
/**
* Produce a consistently formatted frequency slot for chat prefixes.
*
@@ -119,6 +133,28 @@ function normalizeFrequencySlot(value) {
*/
const FREQUENCY_PLACEHOLDER = '   ';
/**
* HTML placeholder for missing preset abbreviations.
* @type {string}
*/
const PRESET_PLACEHOLDER = '  ';
/**
* Canonical preset abbreviations keyed by a normalized preset token.
* @type {Record<string, string>}
*/
const PRESET_ABBREVIATIONS = {
verylongslow: 'VL',
longslow: 'LS',
longmoderate: 'LM',
longfast: 'LF',
mediumslow: 'MS',
mediumfast: 'MF',
shortslow: 'SS',
shortfast: 'SF',
shortturbo: 'ST',
};
/**
* Return the first value in ``candidates`` that is not ``null`` or ``undefined``.
*
@@ -182,6 +218,126 @@ function normalizeFrequency(value) {
return null;
}
/**
* Resolve a modem preset candidate from the provided source object.
*
* @param {*} source Source payload potentially containing modem metadata.
* @param {Set<object>} [visited] Visited references to avoid recursion loops.
* @returns {*|null} Raw modem preset candidate.
*/
function resolveModemPresetCandidate(source, visited = new Set()) {
if (!source || typeof source !== 'object') {
return null;
}
if (visited.has(source)) {
return null;
}
visited.add(source);
const candidate = firstNonNull(
source.modemPreset,
source.modem_preset,
source.modempreset,
source.ModemPreset
);
if (candidate != null) {
return candidate;
}
if (source.node && typeof source.node === 'object') {
const nested = resolveModemPresetCandidate(source.node, visited);
if (nested != null) {
return nested;
}
}
return null;
}
/**
* Convert arbitrary preset input to a trimmed string.
*
* @param {*} value Raw preset candidate.
* @returns {string|null} Clean preset string.
*/
function normalizePresetString(value) {
if (value == null) return null;
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
if (typeof value === 'number') {
return String(value);
}
return null;
}
/**
* Produce a two-character abbreviation for a modem preset.
*
* @param {string} preset Normalized preset string.
* @returns {string|null} Uppercase abbreviation or ``null``.
*/
function abbreviatePreset(preset) {
if (!preset) {
return null;
}
const token = preset.replace(/[^A-Za-z]/g, '').toLowerCase();
if (token && PRESET_ABBREVIATIONS[token]) {
return PRESET_ABBREVIATIONS[token];
}
return derivePresetInitials(preset);
}
/**
* Generate fallback initials for unmapped presets.
*
* @param {string} preset Raw preset string.
* @returns {string|null} Derived initials.
*/
function derivePresetInitials(preset) {
if (!preset) {
return null;
}
const spaced = preset.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
const tokens = spaced
.split(/[\s_-]+/)
.map(part => part.replace(/[^A-Za-z]/g, ''))
.filter(Boolean);
if (tokens.length === 0) {
return null;
}
if (tokens.length === 1) {
const upper = tokens[0].toUpperCase();
if (upper.length >= 2) {
return upper.slice(0, 2);
}
if (upper.length === 1) {
return `${upper}?`;
}
return null;
}
const initials = tokens.map(part => part[0].toUpperCase());
if (initials.length >= 2) {
return `${initials[0]}${initials[1]}`;
}
return null;
}
/**
* Normalise the preset slot contents for the bracket display.
*
* @param {*} value Raw preset code.
* @returns {string} HTML-ready preset slot.
*/
function normalizePresetSlot(value) {
if (value == null) {
return PRESET_PLACEHOLDER;
}
const trimmed = String(value).trim().toUpperCase();
return trimmed.length > 0 ? trimmed.slice(0, 2) : PRESET_PLACEHOLDER;
}
export const __test__ = {
firstNonNull,
normalizeString,
@@ -190,5 +346,11 @@ export const __test__ = {
formatNodeAnnouncementPrefix,
normalizeFrequencySlot,
FREQUENCY_PLACEHOLDER,
formatChatChannelTag
formatChatChannelTag,
resolveModemPresetCandidate,
normalizePresetString,
abbreviatePreset,
derivePresetInitials,
normalizePresetSlot,
PRESET_PLACEHOLDER
};
+6 -3
View File
@@ -34,7 +34,8 @@ import { createMessageNodeHydrator } from './message-node-hydrator.js';
import {
extractChatMessageMetadata,
formatChatMessagePrefix,
formatNodeAnnouncementPrefix
formatNodeAnnouncementPrefix,
formatChatPresetTag
} from './chat-format.js';
import { initializeInstanceSelector } from './instance-selector.js';
import { CHAT_LOG_ENTRY_TYPES, buildChatTabModel, MAX_CHANNEL_INDEX } from './chat-log-tabs.js';
@@ -2333,10 +2334,11 @@ let messagesById = new Map();
timestamp: escapeHtml(ts),
frequency: metadata.frequency ? escapeHtml(metadata.frequency) : ''
});
const presetTag = formatChatPresetTag({ presetCode: metadata.presetCode });
const longNameDisplay = longName != null ? String(longName) : '';
const shortHtml = renderShortHtml(shortName, role, longNameDisplay, nodeData || metadataSource || {});
div.className = 'chat-entry-node';
div.innerHTML = `${prefix} ${shortHtml} ${messageHtml}`;
div.innerHTML = `${prefix}${presetTag} ${shortHtml} ${messageHtml}`;
return div;
}
@@ -2619,8 +2621,9 @@ let messagesById = new Map();
timestamp: escapeHtml(ts),
frequency: metadata.frequency ? escapeHtml(metadata.frequency) : ''
});
const presetTag = formatChatPresetTag({ presetCode: metadata.presetCode });
div.className = 'chat-entry-msg';
div.innerHTML = `${prefix} ${short} ${text}`;
div.innerHTML = `${prefix}${presetTag} ${short} ${text}`;
return div;
}