mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-09 10:22:52 +02:00
Add preset mode to logs (#420)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user