mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
handle naming when primary channel has a name (#422)
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -69,3 +69,7 @@ ai_docs/
|
||||
|
||||
# Generated credentials for the instance
|
||||
web/.config
|
||||
|
||||
# JavaScript dependencies
|
||||
node_modules/
|
||||
web/node_modules/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user