handle naming when primary channel has a name (#422)

This commit is contained in:
l5y
2025-11-08 09:44:41 +01:00
committed by GitHub
parent 6b72b1b3da
commit 3daadc4f68
4 changed files with 111 additions and 20 deletions

4
.gitignore vendored
View File

@@ -69,3 +69,7 @@ ai_docs/
# Generated credentials for the instance
web/.config
# JavaScript dependencies
node_modules/
web/node_modules/

View File

@@ -40,6 +40,8 @@ function fixtureNodes() {
function fixtureMessages() {
return [
{ id: 'recent-default', rx_time: NOW - 5, channel: 0, channel_name: ' MediumFast ' },
{ id: 'primary-preset', rx_time: NOW - 8, channel: 0, modem_preset: ' ShortFast ' },
{ id: 'env-default', rx_time: NOW - 12, channel: 0 },
{ id: 'recent-alt', rx_time: NOW - 10, channel_index: '1', channel_name: ' BerlinMesh ' },
{ id: 'stale', rx_time: NOW - WINDOW - 5, channel: 2 },
{ id: 'encrypted', rx_time: NOW - 20, channel: 3, encrypted: true },
@@ -55,6 +57,7 @@ function buildModel(overrides = {}) {
messages: fixtureMessages(),
nowSeconds: NOW,
windowSeconds: WINDOW,
primaryChannelFallbackLabel: '#EnvDefault',
...overrides
});
}
@@ -72,23 +75,39 @@ test('buildChatTabModel returns sorted nodes and channel buckets', () => {
['recent-node', 'iso-node', 'encrypted']
);
assert.equal(model.channels.length, 3);
const [fallbackChannel, namedPrimaryChannel, secondaryChannel] = model.channels;
assert.equal(model.channels.length, 5);
assert.deepEqual(model.channels.map(channel => channel.label), [
'EnvDefault',
'Fallback',
'MediumFast',
'ShortFast',
'BerlinMesh'
]);
const channelByLabel = Object.fromEntries(model.channels.map(channel => [channel.label, channel]));
const envChannel = channelByLabel.EnvDefault;
assert.equal(envChannel.index, 0);
assert.equal(envChannel.id, 'channel-0-envdefault');
assert.deepEqual(envChannel.entries.map(entry => entry.message.id), ['env-default']);
const fallbackChannel = channelByLabel.Fallback;
assert.equal(fallbackChannel.index, 0);
assert.equal(fallbackChannel.label, 'Fallback');
assert.equal(fallbackChannel.id, 'channel-0-fallback');
assert.equal(fallbackChannel.entries.length, 1);
assert.deepEqual(fallbackChannel.entries.map(entry => entry.message.id), ['no-index']);
const namedPrimaryChannel = channelByLabel.MediumFast;
assert.equal(namedPrimaryChannel.index, 0);
assert.equal(namedPrimaryChannel.label, 'MediumFast');
assert.equal(namedPrimaryChannel.id, 'channel-0-mediumfast');
assert.equal(namedPrimaryChannel.entries.length, 1);
assert.deepEqual(namedPrimaryChannel.entries.map(entry => entry.message.id), ['recent-default']);
const presetChannel = channelByLabel.ShortFast;
assert.equal(presetChannel.index, 0);
assert.equal(presetChannel.id, 'channel-0-shortfast');
assert.deepEqual(presetChannel.entries.map(entry => entry.message.id), ['primary-preset']);
const secondaryChannel = channelByLabel.BerlinMesh;
assert.equal(secondaryChannel.index, 1);
assert.equal(secondaryChannel.label, 'BerlinMesh');
assert.equal(secondaryChannel.id, 'channel-1');
assert.equal(secondaryChannel.entries.length, 2);
assert.deepEqual(secondaryChannel.entries.map(entry => entry.message.id), ['iso-ts', 'recent-alt']);
@@ -101,6 +120,19 @@ test('buildChatTabModel always includes channel zero bucket', () => {
assert.equal(model.channels[0].entries.length, 0);
});
test('buildChatTabModel falls back to numeric label when no metadata provided', () => {
const model = buildChatTabModel({
nodes: [],
messages: [{ id: 'plain', rx_time: NOW - 5, channel: 0 }],
nowSeconds: NOW,
windowSeconds: WINDOW,
primaryChannelFallbackLabel: ''
});
assert.equal(model.channels.length, 1);
assert.equal(model.channels[0].label, '0');
assert.equal(model.channels[0].id, 'channel-0');
});
test('normaliseChannelIndex handles numeric and textual input', () => {
assert.equal(normaliseChannelIndex(2.9), 2);
assert.equal(normaliseChannelIndex(' 7 '), 7);

View File

@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { extractModemMetadata } from './node-modem-metadata.js';
/**
* Highest channel index that should be represented within the tab view.
* @type {number}
@@ -54,7 +56,8 @@ export const CHAT_LOG_ENTRY_TYPES = Object.freeze({
* messages?: Array<Object>,
* nowSeconds: number,
* windowSeconds: number,
* maxChannelIndex?: number
* maxChannelIndex?: number,
* primaryChannelFallbackLabel?: string|null
* }} params Aggregation inputs.
* @returns {{
* logEntries: Array<{ ts: number, type: string, nodeId?: string, nodeNum?: number }>,
@@ -69,11 +72,13 @@ export function buildChatTabModel({
messages = [],
nowSeconds,
windowSeconds,
maxChannelIndex = MAX_CHANNEL_INDEX
maxChannelIndex = MAX_CHANNEL_INDEX,
primaryChannelFallbackLabel = null
}) {
const cutoff = (Number.isFinite(nowSeconds) ? nowSeconds : 0) - (Number.isFinite(windowSeconds) ? windowSeconds : 0);
const logEntries = [];
const channelBuckets = new Map();
const primaryChannelEnvLabel = normalisePrimaryChannelEnvLabel(primaryChannelFallbackLabel);
for (const node of nodes || []) {
if (!node) continue;
@@ -142,22 +147,32 @@ export function buildChatTabModel({
message.channel_name ?? message.channelName ?? message.channel_display ?? message.channelDisplay
);
const safeIndex = channelIndex != null && channelIndex >= 0 ? channelIndex : 0;
const bucketKey = buildChannelBucketKey(safeIndex, channelName);
const modemPreset = safeIndex === 0 ? extractModemMetadata(message).modemPreset : null;
const labelInfo = resolveChannelLabel({
index: safeIndex,
channelName,
modemPreset,
envFallbackLabel: primaryChannelEnvLabel
});
const bucketKey = buildChannelBucketKey(safeIndex, safeIndex === 0 && labelInfo.label !== '0' ? labelInfo.label : null);
let bucket = channelBuckets.get(bucketKey);
if (!bucket) {
bucket = {
key: bucketKey,
id: buildChannelTabId(bucketKey),
index: safeIndex,
label: channelName || String(safeIndex),
label: labelInfo.label,
entries: [],
hasExplicitName: Boolean(channelName),
labelPriority: labelInfo.priority,
isPrimaryFallback: bucketKey === '0'
};
channelBuckets.set(bucketKey, bucket);
} else if (channelName && !bucket.hasExplicitName) {
bucket.label = channelName;
bucket.hasExplicitName = true;
} else {
const existingPriority = bucket.labelPriority ?? CHANNEL_LABEL_PRIORITY.INDEX;
if ((labelInfo.priority ?? CHANNEL_LABEL_PRIORITY.INDEX) > existingPriority) {
bucket.label = labelInfo.label;
bucket.labelPriority = labelInfo.priority;
}
}
bucket.entries.push({ ts, message });
@@ -180,7 +195,7 @@ export function buildChatTabModel({
index: 0,
label: '0',
entries: [],
hasExplicitName: false,
labelPriority: CHANNEL_LABEL_PRIORITY.INDEX,
isPrimaryFallback: true
});
}
@@ -307,10 +322,13 @@ export function normaliseChannelName(value) {
return null;
}
function buildChannelBucketKey(index, channelName) {
function buildChannelBucketKey(index, primaryChannelLabel) {
const safeIndex = Number.isFinite(index) ? Math.max(0, Math.trunc(index)) : 0;
if (safeIndex === 0 && channelName) {
return `0::${channelName.toLowerCase()}`;
if (safeIndex === 0 && primaryChannelLabel) {
const trimmed = primaryChannelLabel.trim();
if (trimmed.length > 0 && trimmed !== '0') {
return `0::${trimmed.toLowerCase()}`;
}
}
return String(safeIndex);
}
@@ -353,6 +371,42 @@ function hashChannelKey(value) {
return hash.toString(36);
}
const CHANNEL_LABEL_PRIORITY = Object.freeze({
INDEX: 0,
ENV: 1,
MODEM: 2,
NAME: 3
});
function resolveChannelLabel({ index, channelName, modemPreset, envFallbackLabel }) {
const safeIndex = Number.isFinite(index) ? Math.max(0, Math.trunc(index)) : 0;
if (safeIndex === 0) {
if (channelName) {
return { label: channelName, priority: CHANNEL_LABEL_PRIORITY.NAME };
}
if (modemPreset) {
return { label: modemPreset, priority: CHANNEL_LABEL_PRIORITY.MODEM };
}
if (envFallbackLabel) {
return { label: envFallbackLabel, priority: CHANNEL_LABEL_PRIORITY.ENV };
}
return { label: '0', priority: CHANNEL_LABEL_PRIORITY.INDEX };
}
if (channelName) {
return { label: channelName, priority: CHANNEL_LABEL_PRIORITY.NAME };
}
return { label: String(safeIndex), priority: CHANNEL_LABEL_PRIORITY.INDEX };
}
function normalisePrimaryChannelEnvLabel(value) {
const trimmed = normaliseChannelName(value);
if (!trimmed) {
return null;
}
const withoutHash = trimmed.replace(/^#+/, '').trim();
return withoutHash.length > 0 ? withoutHash : null;
}
export const __test__ = {
resolveTimestampSeconds,
normaliseChannelIndex,

View File

@@ -2727,7 +2727,8 @@ let messagesById = new Map();
messages,
nowSeconds,
windowSeconds: CHAT_RECENT_WINDOW_SECONDS,
maxChannelIndex: MAX_CHANNEL_INDEX
maxChannelIndex: MAX_CHANNEL_INDEX,
primaryChannelFallbackLabel: config.channel
});
const enrichedLogEntries = attachNodeContextToLogEntries(logEntries);