Fix review items 001/003/005 for decoder, channel labels, and node filters

This commit is contained in:
yellowcooln
2026-03-04 20:07:37 -05:00
parent c22274c4e5
commit 2a380f88b4
7 changed files with 65 additions and 54 deletions

View File

@@ -55,8 +55,6 @@ async def list_nodes(
Node.adv_type == "repeater",
Node.adv_type.ilike("%repeater%"),
Node.adv_type.ilike("%relay%"),
Node.name.ilike("%repeater%"),
Node.name.ilike("%relay%"),
)
)
elif normalized_adv_type == "companion":
@@ -65,8 +63,6 @@ async def list_nodes(
Node.adv_type == "companion",
Node.adv_type.ilike("%companion%"),
Node.adv_type.ilike("%observer%"),
Node.name.ilike("%companion%"),
Node.name.ilike("%observer%"),
)
)
elif normalized_adv_type == "room":
@@ -74,7 +70,6 @@ async def list_nodes(
or_(
Node.adv_type == "room",
Node.adv_type.ilike("%room%"),
Node.name.ilike("%room%"),
)
)
elif normalized_adv_type == "chat":

View File

@@ -183,6 +183,9 @@ class LetsMeshPacketDecoder:
clean_hex = raw_hex.strip()
if not clean_hex:
return None
if not self._is_hex(clean_hex):
logger.debug("LetsMesh decoder skipped non-hex raw payload")
return None
cached = self._decode_cache.get(clean_hex)
if clean_hex in self._decode_cache:
return cached

View File

@@ -22,6 +22,34 @@ export function getConfig() {
return window.__APP_CONFIG__ || {};
}
/**
* Build channel label map from app config.
* Keys are numeric channel indexes and values are non-empty labels.
*
* @param {Object} [config]
* @returns {Map<number, string>}
*/
export function getChannelLabelsMap(config = getConfig()) {
return new Map(
Object.entries(config.channel_labels || {})
.map(([idx, label]) => [parseInt(idx, 10), typeof label === 'string' ? label.trim() : ''])
.filter(([idx, label]) => Number.isInteger(idx) && label.length > 0),
);
}
/**
* Resolve a channel label from a numeric index.
*
* @param {number|string} channelIdx
* @param {Map<number, string>} [channelLabels]
* @returns {string|null}
*/
export function resolveChannelLabel(channelIdx, channelLabels = getChannelLabelsMap()) {
const parsed = parseInt(String(channelIdx), 10);
if (!Number.isInteger(parsed)) return null;
return channelLabels.get(parsed) || null;
}
/**
* Parse API datetime strings reliably.
* MeshCore API often returns UTC timestamps without an explicit timezone suffix.

View File

@@ -1,35 +1,17 @@
import { apiGet } from '../api.js';
import {
html, litRender, nothing,
getConfig, typeEmoji, errorAlert, pageColors, t, formatDateTime,
getConfig, getChannelLabelsMap, resolveChannelLabel,
typeEmoji, errorAlert, pageColors, t, formatDateTime,
} from '../components.js';
import {
iconNodes, iconAdvertisements, iconMessages, iconChannel,
} from '../icons.js';
function knownChannelLabel(channelIdx) {
const config = getConfig();
const configuredChannelLabels = new Map(
Object.entries(config.channel_labels || {})
.map(([idx, label]) => [parseInt(idx, 10), typeof label === 'string' ? label.trim() : ''])
.filter(([idx, label]) => Number.isInteger(idx) && label.length > 0),
);
const builtInChannelLabels = new Map([
[17, 'Public'],
[217, '#test'],
[202, '#bot'],
[184, '#chat'],
[159, '#jokes'],
[221, '#sports'],
[104, '#emergency'],
]);
return configuredChannelLabels.get(channelIdx) || builtInChannelLabels.get(channelIdx) || null;
}
function channelLabel(channel) {
function channelLabel(channel, channelLabels) {
const idx = parseInt(String(channel), 10);
if (Number.isInteger(idx)) {
return knownChannelLabel(idx) || `Ch ${idx}`;
return resolveChannelLabel(idx, channelLabels) || `Ch ${idx}`;
}
return String(channel);
}
@@ -84,11 +66,11 @@ function renderRecentAds(ads) {
</div>`;
}
function renderChannelMessages(channelMessages) {
function renderChannelMessages(channelMessages, channelLabels) {
if (!channelMessages || Object.keys(channelMessages).length === 0) return nothing;
const channels = Object.entries(channelMessages).map(([channel, messages]) => {
const label = channelLabel(channel);
const label = channelLabel(channel, channelLabels);
const msgLines = messages.map(msg => html`
<div class="text-sm">
<span class="text-xs opacity-50">${formatTimeShort(msg.received_at)}</span>
@@ -127,6 +109,7 @@ function gridCols(count) {
export async function render(container, params, router) {
try {
const config = getConfig();
const channelLabels = getChannelLabelsMap(config);
const features = config.features || {};
const showNodes = features.nodes !== false;
const showAdverts = features.advertisements !== false;
@@ -242,7 +225,7 @@ ${bottomCount > 0 ? html`
</div>
</div>` : nothing}
${showMessages ? renderChannelMessages(stats.channel_messages) : nothing}
${showMessages ? renderChannelMessages(stats.channel_messages, channelLabels) : nothing}
</div>` : nothing}`, container);
window.initDashboardCharts(

View File

@@ -2,6 +2,7 @@ import { apiGet } from '../api.js';
import {
html, litRender, nothing, t,
getConfig, formatDateTime, formatDateTimeShort,
getChannelLabelsMap, resolveChannelLabel,
truncateKey, errorAlert,
pagination, timezoneIndicator,
createFilterHandler, autoSubmit, submitOnEnter
@@ -16,27 +17,10 @@ export async function render(container, params, router) {
const offset = (page - 1) * limit;
const config = getConfig();
const channelLabels = getChannelLabelsMap(config);
const tz = config.timezone || '';
const tzBadge = tz && tz !== 'UTC' ? html`<span class="text-sm opacity-60">${tz}</span>` : nothing;
const navigate = (url) => router.navigate(url);
const configuredChannelLabels = new Map(
Object.entries(config.channel_labels || {})
.map(([idx, label]) => [parseInt(idx, 10), typeof label === 'string' ? label.trim() : ''])
.filter(([idx, label]) => Number.isInteger(idx) && label.length > 0),
);
const builtInChannelLabels = new Map([
[17, 'Public'],
[217, '#test'],
[202, '#bot'],
[184, '#chat'],
[159, '#jokes'],
[221, '#sports'],
[104, '#emergency'],
]);
function knownChannelLabel(channelIdx) {
return configuredChannelLabels.get(channelIdx) || builtInChannelLabels.get(channelIdx) || null;
}
function channelInfo(msg) {
if (msg.message_type !== 'channel') {
@@ -45,7 +29,7 @@ export async function render(container, params, router) {
const rawText = msg.text || '';
const match = rawText.match(/^\[([^\]]+)\]\s+([\s\S]*)$/);
if (msg.channel_idx !== null && msg.channel_idx !== undefined) {
const knownLabel = knownChannelLabel(msg.channel_idx);
const knownLabel = resolveChannelLabel(msg.channel_idx, channelLabels);
if (knownLabel) {
return {
label: knownLabel,
@@ -63,7 +47,7 @@ export async function render(container, params, router) {
};
}
if (msg.channel_idx !== null && msg.channel_idx !== undefined) {
const knownLabel = knownChannelLabel(msg.channel_idx);
const knownLabel = resolveChannelLabel(msg.channel_idx, channelLabels);
return { label: knownLabel || `Ch ${msg.channel_idx}`, text: rawText || '-' };
}
return { label: t('messages.type_channel'), text: rawText || '-' };

View File

@@ -105,25 +105,28 @@ class TestListNodesFilters:
def test_filter_by_adv_type_matches_legacy_labels(
self, client_no_auth, api_db_session
):
"""Canonical adv_type filters match legacy LetsMesh values and names."""
"""Canonical adv_type filters match legacy LetsMesh adv_type values only."""
from datetime import datetime, timezone
from meshcore_hub.common.models import Node
repeater_node = Node(
public_key="ab" * 32,
name="Car Relay",
adv_type="PyMC-Repeater",
first_seen=datetime.now(timezone.utc),
)
companion_node = Node(
public_key="cd" * 32,
name="YC-Observer",
adv_type="offline",
adv_type="offline companion",
first_seen=datetime.now(timezone.utc),
)
room_node = Node(
public_key="ef" * 32,
adv_type="room server",
first_seen=datetime.now(timezone.utc),
)
name_only_room_node = Node(
public_key="12" * 32,
name="WAL-SE Room Server",
adv_type="unknown",
first_seen=datetime.now(timezone.utc),
@@ -131,6 +134,7 @@ class TestListNodesFilters:
api_db_session.add(repeater_node)
api_db_session.add(companion_node)
api_db_session.add(room_node)
api_db_session.add(name_only_room_node)
api_db_session.commit()
response = client_no_auth.get("/api/v1/nodes?adv_type=repeater")
@@ -147,6 +151,7 @@ class TestListNodesFilters:
assert response.status_code == 200
room_keys = {item["public_key"] for item in response.json()["items"]}
assert room_node.public_key in room_keys
assert name_only_room_node.public_key not in room_keys
def test_filter_by_member_id(self, client_no_auth, sample_node_with_member_tag):
"""Test filtering nodes by member_id tag."""

View File

@@ -12,6 +12,19 @@ def test_decode_payload_returns_none_without_raw() -> None:
assert decoder.decode_payload({"packet_type": 5}) is None
def test_decode_payload_rejects_non_hex_raw_without_invoking_decoder() -> None:
"""Decoder returns None and does not execute subprocess for invalid raw hex."""
decoder = LetsMeshPacketDecoder(enabled=True, command="meshcore-decoder")
with (
patch("meshcore_hub.collector.letsmesh_decoder.shutil.which", return_value="1"),
patch("meshcore_hub.collector.letsmesh_decoder.subprocess.run") as mock_run,
):
assert decoder.decode_payload({"raw": "ZZ-not-hex"}) is None
mock_run.assert_not_called()
def test_decode_payload_invokes_decoder_with_keys() -> None:
"""Decoder command includes channel keys and returns parsed JSON."""
decoder = LetsMeshPacketDecoder(