mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-03-28 17:42:56 +01:00
Fix review items 001/003/005 for decoder, channel labels, and node filters
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 || '-' };
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user