mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-11 11:13:04 +02:00
Phase 5: Frontend path rendering
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import { formatTime } from '../utils/messageParser';
|
import { formatTime } from '../utils/messageParser';
|
||||||
import { isValidLocation, calculateDistance, formatDistance } from '../utils/pathUtils';
|
import {
|
||||||
|
isValidLocation,
|
||||||
|
calculateDistance,
|
||||||
|
formatDistance,
|
||||||
|
parsePathHops,
|
||||||
|
} from '../utils/pathUtils';
|
||||||
import { getMapFocusHash } from '../utils/urlHash';
|
import { getMapFocusHash } from '../utils/urlHash';
|
||||||
import { isFavorite } from '../utils/favorites';
|
import { isFavorite } from '../utils/favorites';
|
||||||
import { handleKeyboardActivate } from '../utils/a11y';
|
import { handleKeyboardActivate } from '../utils/a11y';
|
||||||
@@ -413,7 +418,7 @@ export function ContactInfoPane({
|
|||||||
className="flex justify-between items-center text-sm"
|
className="flex justify-between items-center text-sm"
|
||||||
>
|
>
|
||||||
<span className="font-mono text-xs truncate">
|
<span className="font-mono text-xs truncate">
|
||||||
{p.path ? p.path.match(/.{2}/g)!.join(' → ') : '(direct)'}
|
{p.path ? parsePathHops(p.path, p.path_len).join(' → ') : '(direct)'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground flex-shrink-0 ml-2">
|
<span className="text-xs text-muted-foreground flex-shrink-0 ml-2">
|
||||||
{p.heard_count}x · {formatTime(p.last_seen)}
|
{p.heard_count}x · {formatTime(p.last_seen)}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function PathModal({
|
|||||||
const resolvedPaths = hasPaths
|
const resolvedPaths = hasPaths
|
||||||
? paths.map((p) => ({
|
? paths.map((p) => ({
|
||||||
...p,
|
...p,
|
||||||
resolved: resolvePath(p.path, senderInfo, contacts, config),
|
resolved: resolvePath(p.path, senderInfo, contacts, config, p.path_len),
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ export function PathModal({
|
|||||||
{/* Raw path summary */}
|
{/* Raw path summary */}
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{paths.map((p, index) => {
|
{paths.map((p, index) => {
|
||||||
const hops = parsePathHops(p.path);
|
const hops = parsePathHops(p.path, p.path_len);
|
||||||
const rawPath = hops.length > 0 ? hops.join('->') : 'direct';
|
const rawPath = hops.length > 0 ? hops.join('->') : 'direct';
|
||||||
return (
|
return (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
|
|||||||
@@ -150,10 +150,12 @@ export interface ChannelDetail {
|
|||||||
|
|
||||||
/** A single path that a message took to reach us */
|
/** A single path that a message took to reach us */
|
||||||
export interface MessagePath {
|
export interface MessagePath {
|
||||||
/** Hex-encoded routing path (2 chars per hop) */
|
/** Hex-encoded routing path */
|
||||||
path: string;
|
path: string;
|
||||||
/** Unix timestamp when this path was received */
|
/** Unix timestamp when this path was received */
|
||||||
received_at: number;
|
received_at: number;
|
||||||
|
/** Hop count (number of intermediate nodes). Null for legacy data (infer as len(path)/2). */
|
||||||
|
path_len?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Contact, RadioConfig, MessagePath } from '../types';
|
|||||||
import { CONTACT_TYPE_REPEATER } from '../types';
|
import { CONTACT_TYPE_REPEATER } from '../types';
|
||||||
|
|
||||||
export interface PathHop {
|
export interface PathHop {
|
||||||
prefix: string; // 2-char hex prefix (e.g., "1A")
|
prefix: string; // Hex hop identifier (e.g., "1A" for 1-byte, "1A2B" for 2-byte)
|
||||||
matches: Contact[]; // Matched repeaters (empty=unknown, multiple=ambiguous)
|
matches: Contact[]; // Matched repeaters (empty=unknown, multiple=ambiguous)
|
||||||
distanceFromPrev: number | null; // km from previous hop
|
distanceFromPrev: number | null; // km from previous hop
|
||||||
}
|
}
|
||||||
@@ -30,22 +30,37 @@ export interface SenderInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split hex string into 2-char hops
|
* Split hex path string into per-hop chunks.
|
||||||
|
*
|
||||||
|
* When hopCount is provided (from path_len metadata), the bytes-per-hop is
|
||||||
|
* derived from the hex length divided by the hop count. This correctly handles
|
||||||
|
* multi-byte hop identifiers (1, 2, or 3 bytes per hop).
|
||||||
|
*
|
||||||
|
* Falls back to 2-char (1-byte) chunks when hopCount is missing or doesn't
|
||||||
|
* divide evenly — matching legacy behavior.
|
||||||
*/
|
*/
|
||||||
export function parsePathHops(path: string | null | undefined): string[] {
|
export function parsePathHops(path: string | null | undefined, hopCount?: number | null): string[] {
|
||||||
if (!path || path.length === 0) {
|
if (!path || path.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = path.toUpperCase();
|
const normalized = path.toUpperCase();
|
||||||
const hops: string[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < normalized.length; i += 2) {
|
// Derive chars-per-hop from metadata when available
|
||||||
if (i + 1 < normalized.length) {
|
let charsPerHop = 2; // default: 1-byte hops
|
||||||
hops.push(normalized.slice(i, i + 2));
|
if (hopCount && hopCount > 0) {
|
||||||
|
const derived = normalized.length / hopCount;
|
||||||
|
// Accept only valid even widths (2, 4, 6) that divide evenly
|
||||||
|
if (derived >= 2 && derived % 2 === 0 && derived * hopCount === normalized.length) {
|
||||||
|
charsPerHop = derived;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hops: string[] = [];
|
||||||
|
for (let i = 0; i + charsPerHop <= normalized.length; i += charsPerHop) {
|
||||||
|
hops.push(normalized.slice(i, i + charsPerHop));
|
||||||
|
}
|
||||||
|
|
||||||
return hops;
|
return hops;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,12 +161,16 @@ function sortContactsByDistance(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get simple hop count from path string
|
* Get hop count from path, using explicit metadata when available.
|
||||||
*/
|
*/
|
||||||
function getHopCount(path: string | null | undefined): number {
|
function getHopCount(path: string | null | undefined, hopCount?: number | null): number {
|
||||||
|
if (hopCount != null && hopCount >= 0) {
|
||||||
|
return hopCount;
|
||||||
|
}
|
||||||
if (!path || path.length === 0) {
|
if (!path || path.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
// Legacy fallback: assume 1-byte (2 hex chars) per hop
|
||||||
return Math.floor(path.length / 2);
|
return Math.floor(path.length / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +189,7 @@ export function formatHopCounts(paths: MessagePath[] | null | undefined): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get hop counts for all paths and sort ascending
|
// Get hop counts for all paths and sort ascending
|
||||||
const hopCounts = paths.map((p) => getHopCount(p.path)).sort((a, b) => a - b);
|
const hopCounts = paths.map((p) => getHopCount(p.path, p.path_len)).sort((a, b) => a - b);
|
||||||
|
|
||||||
const allDirect = hopCounts.every((h) => h === 0);
|
const allDirect = hopCounts.every((h) => h === 0);
|
||||||
const hasMultiple = paths.length > 1;
|
const hasMultiple = paths.length > 1;
|
||||||
@@ -189,9 +208,10 @@ export function resolvePath(
|
|||||||
path: string | null | undefined,
|
path: string | null | undefined,
|
||||||
sender: SenderInfo,
|
sender: SenderInfo,
|
||||||
contacts: Contact[],
|
contacts: Contact[],
|
||||||
config: RadioConfig | null
|
config: RadioConfig | null,
|
||||||
|
hopCount?: number | null
|
||||||
): ResolvedPath {
|
): ResolvedPath {
|
||||||
const hopPrefixes = parsePathHops(path);
|
const hopPrefixes = parsePathHops(path, hopCount);
|
||||||
|
|
||||||
// Build sender info
|
// Build sender info
|
||||||
const senderPrefix = sender.publicKeyOrPrefix.toUpperCase().slice(0, 2);
|
const senderPrefix = sender.publicKeyOrPrefix.toUpperCase().slice(0, 2);
|
||||||
|
|||||||
Reference in New Issue
Block a user