feat: show "Scope: unknown" for transport-scoped messages with no match

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").
This commit is contained in:
dborup
2026-07-17 15:29:41 +02:00
parent 8765de71e0
commit a94d57ed4a
4 changed files with 19 additions and 3 deletions
+5 -3
View File
@@ -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,
}
+1
View File
@@ -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,
+1
View File
@@ -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 {
+12
View File
@@ -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 /