From a94d57ed4a431ddaf2f5cc4773147da503d4bd72 Mon Sep 17 00:00:00 2001 From: dborup Date: Fri, 17 Jul 2026 15:29:41 +0200 Subject: [PATCH] feat: show "Scope: unknown" for transport-scoped messages with no match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetChannelMessages already returned an empty scope string for transport-eligible packets whose region couldn't be determined (no configured region matched, or matchScope now reports an HMAC collision as unknown), but the UI treated empty scope the same as "not applicable" and rendered nothing — indistinguishable from a plain FLOOD/DIRECT message that never carries a scope at all. Adds route_type to the channel-message payload (both SQLite and in-memory paths, plus the decrypt-candidate and live WS paths) so the frontend can tell "not transport-scoped" (routeType 1/2, no tag) apart from "transport-scoped but unresolved" (routeType 0/3 with empty scope, now shown as "Scope: unknown"). --- cmd/server/db.go | 8 +++++--- cmd/server/store.go | 1 + cmd/server/types.go | 1 + public/channels.js | 12 ++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/cmd/server/db.go b/cmd/server/db.go index 4226c5fe..14cac6fb 100644 --- a/cmd/server/db.go +++ b/cmd/server/db.go @@ -1863,7 +1863,7 @@ func (db *DB) GetChannelMessages(channelHash string, limit, offset int, region . var obsSQL string if db.isV3 { obsSQL = `SELECT o.id, t.id, t.hash, t.decoded_json, t.first_seen, - obs.id, obs.name, o.snr, o.path_json, o.timestamp` + scopeCol + ` + obs.id, obs.name, o.snr, o.path_json, o.timestamp, t.route_type` + scopeCol + ` FROM observations o JOIN transmissions t ON t.id = o.transmission_id LEFT JOIN observers obs ON obs.rowid = o.observer_idx @@ -1871,7 +1871,7 @@ func (db *DB) GetChannelMessages(channelHash string, limit, offset int, region . ORDER BY o.id ASC` } else { obsSQL = `SELECT o.id, t.id, t.hash, t.decoded_json, t.first_seen, - o.observer_id, o.observer_name, o.snr, o.path_json, o.timestamp` + scopeCol + ` + o.observer_id, o.observer_name, o.snr, o.path_json, o.timestamp, t.route_type` + scopeCol + ` FROM observations o JOIN transmissions t ON t.id = o.transmission_id WHERE t.id IN (` + strings.Join(idPlaceholders, ",") + `) @@ -1896,8 +1896,9 @@ func (db *DB) GetChannelMessages(channelHash string, limit, offset int, region . var pktHash, dj, fs, obsID, obsName, pathJSON sql.NullString var snr sql.NullFloat64 var obsTs sql.NullInt64 + var routeType sql.NullInt64 var scopeName sql.NullString - scanArgs := []interface{}{&pktID, &txID, &pktHash, &dj, &fs, &obsID, &obsName, &snr, &pathJSON, &obsTs} + scanArgs := []interface{}{&pktID, &txID, &pktHash, &dj, &fs, &obsID, &obsName, &snr, &pathJSON, &obsTs, &routeType} if db.hasScopeName { scanArgs = append(scanArgs, &scopeName) } @@ -1953,6 +1954,7 @@ func (db *DB) GetChannelMessages(channelHash string, limit, offset int, region . "hops": hops, "snr": nullFloat(snr), "scope": nullStr(scopeName), + "routeType": nullInt(routeType), }, Repeats: 1, } diff --git a/cmd/server/store.go b/cmd/server/store.go index 5ad623c0..d7ad4307 100644 --- a/cmd/server/store.go +++ b/cmd/server/store.go @@ -5519,6 +5519,7 @@ func (s *PacketStore) GetChannelMessages(channelHash string, limit, offset int, "hops": hops, "snr": snrVal, "scope": strOrNil(tx.ScopeName), + "routeType": intPtrOrNil(tx.RouteType), }, Repeats: 1, Observers: observers, diff --git a/cmd/server/types.go b/cmd/server/types.go index 54738397..28537bb6 100644 --- a/cmd/server/types.go +++ b/cmd/server/types.go @@ -906,6 +906,7 @@ type ChannelMessageResp struct { Hops int `json:"hops"` SNR interface{} `json:"snr"` Scope interface{} `json:"scope"` + RouteType interface{} `json:"routeType"` } type ChannelMessagesResponse struct { diff --git a/public/channels.js b/public/channels.js index 23205825..40abb9db 100644 --- a/public/channels.js +++ b/public/channels.js @@ -664,6 +664,7 @@ hops: d.path_len || 0, snr: c.packet.snr || null, observers: c.packet.observer_name ? [c.packet.observer_name] : [], scope: c.packet.scope_name || null, + routeType: c.packet.route_type ?? null, repeats: 1 }); continue; @@ -681,6 +682,7 @@ hops: 0, snr: c.packet.snr || null, observers: c.packet.observer_name ? [c.packet.observer_name] : [], scope: c.packet.scope_name || null, + routeType: c.packet.route_type ?? null, repeats: 1 }); } else { @@ -1420,6 +1422,7 @@ var snr = m.data?.snr ?? m.data?.packet?.snr ?? payload.SNR ?? null; var observer = m.data?.packet?.observer_name || m.data?.observer || null; var scope = m.data?.scope_name || m.data?.packet?.scope_name || null; + var routeType = m.data?.route_type ?? m.data?.packet?.route_type ?? null; // Update channel list entry — only once per unique packet hash var isFirstObservation = pktHash && !seenHashes.has(pktHash + ':' + channelKey); @@ -1472,6 +1475,7 @@ hops: payload.path_len || 0, snr: snr, scope: scope, + routeType: routeType, // #1498: mark as WS-pushed so a later REST replacement // (selectChannel / refreshMessages) can merge instead of // stomp. Without this flag the REST response wipes any @@ -2262,7 +2266,15 @@ if (msg.observers?.length > 1) meta.push(`${msg.observers.length} observers`); if (msg.hops > 0) meta.push(`${msg.hops} hops`); if (msg.snr !== null && msg.snr !== undefined) meta.push(`SNR ${msg.snr}`); + // Scope only applies to TRANSPORT_FLOOD(0)/TRANSPORT_DIRECT(3) routes. + // Plain FLOOD(1)/DIRECT(2) never carry a scope — show nothing for those. + // A transport-eligible message with an empty scope means the region + // couldn't be determined (no configured region matched, or an + // HMAC collision made the match ambiguous) — show it as unknown + // rather than silently omitting the tag. + const isTransportRoute = msg.routeType === 0 || msg.routeType === 3; if (msg.scope) meta.push(`Scope: ${escapeHtml(msg.scope)}`); + else if (isTransportRoute) meta.push('Scope: unknown'); const safeId = btoa(encodeURIComponent(sender)); // #1367: emit BOTH the new chat-app class names (.ch-message /