mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Add radio model and stats display. Closes #64
This commit is contained in:
10
app/radio.py
10
app/radio.py
@@ -131,6 +131,11 @@ class RadioManager:
|
||||
self._setup_lock: asyncio.Lock | None = None
|
||||
self._setup_in_progress: bool = False
|
||||
self._setup_complete: bool = False
|
||||
self.device_info_loaded: bool = False
|
||||
self.max_contacts: int | None = None
|
||||
self.device_model: str | None = None
|
||||
self.firmware_build: str | None = None
|
||||
self.firmware_version: str | None = None
|
||||
self.max_channels: int = 40
|
||||
self.path_hash_mode: int = 0
|
||||
self.path_hash_mode_supported: bool = False
|
||||
@@ -488,6 +493,11 @@ class RadioManager:
|
||||
await self._disable_meshcore_auto_reconnect(mc)
|
||||
self._meshcore = None
|
||||
self._setup_complete = False
|
||||
self.device_info_loaded = False
|
||||
self.max_contacts = None
|
||||
self.device_model = None
|
||||
self.firmware_build = None
|
||||
self.firmware_version = None
|
||||
self.max_channels = 40
|
||||
self.path_hash_mode = 0
|
||||
self.path_hash_mode_supported = False
|
||||
|
||||
@@ -11,18 +11,34 @@ from app.services.radio_runtime import radio_runtime as radio_manager
|
||||
router = APIRouter(tags=["health"])
|
||||
|
||||
|
||||
class RadioDeviceInfoResponse(BaseModel):
|
||||
model: str | None = None
|
||||
firmware_build: str | None = None
|
||||
firmware_version: str | None = None
|
||||
max_contacts: int | None = None
|
||||
max_channels: int | None = None
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
radio_connected: bool
|
||||
radio_initializing: bool = False
|
||||
radio_state: str = "disconnected"
|
||||
connection_info: str | None
|
||||
radio_device_info: RadioDeviceInfoResponse | None = None
|
||||
database_size_mb: float
|
||||
oldest_undecrypted_timestamp: int | None
|
||||
fanout_statuses: dict[str, dict[str, str]] = {}
|
||||
bots_disabled: bool = False
|
||||
|
||||
|
||||
def _clean_optional_str(value: object) -> str | None:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
cleaned = value.strip()
|
||||
return cleaned or None
|
||||
|
||||
|
||||
async def build_health_data(radio_connected: bool, connection_info: str | None) -> dict:
|
||||
"""Build the health status payload used by REST endpoint and WebSocket broadcasts."""
|
||||
db_size_mb = 0.0
|
||||
@@ -48,22 +64,12 @@ async def build_health_data(radio_connected: bool, connection_info: str | None)
|
||||
pass
|
||||
|
||||
setup_in_progress = getattr(radio_manager, "is_setup_in_progress", False)
|
||||
if not isinstance(setup_in_progress, bool):
|
||||
setup_in_progress = False
|
||||
|
||||
setup_complete = getattr(radio_manager, "is_setup_complete", radio_connected)
|
||||
if not isinstance(setup_complete, bool):
|
||||
setup_complete = radio_connected
|
||||
if not radio_connected:
|
||||
setup_complete = False
|
||||
|
||||
connection_desired = getattr(radio_manager, "connection_desired", True)
|
||||
if not isinstance(connection_desired, bool):
|
||||
connection_desired = True
|
||||
|
||||
is_reconnecting = getattr(radio_manager, "is_reconnecting", False)
|
||||
if not isinstance(is_reconnecting, bool):
|
||||
is_reconnecting = False
|
||||
|
||||
radio_initializing = bool(radio_connected and (setup_in_progress or not setup_complete))
|
||||
if not connection_desired:
|
||||
@@ -77,12 +83,26 @@ async def build_health_data(radio_connected: bool, connection_info: str | None)
|
||||
else:
|
||||
radio_state = "disconnected"
|
||||
|
||||
radio_device_info = None
|
||||
device_info_loaded = getattr(radio_manager, "device_info_loaded", False)
|
||||
if radio_connected and device_info_loaded:
|
||||
radio_device_info = {
|
||||
"model": _clean_optional_str(getattr(radio_manager, "device_model", None)),
|
||||
"firmware_build": _clean_optional_str(getattr(radio_manager, "firmware_build", None)),
|
||||
"firmware_version": _clean_optional_str(
|
||||
getattr(radio_manager, "firmware_version", None)
|
||||
),
|
||||
"max_contacts": getattr(radio_manager, "max_contacts", None),
|
||||
"max_channels": getattr(radio_manager, "max_channels", None),
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "ok" if radio_connected and not radio_initializing else "degraded",
|
||||
"radio_connected": radio_connected,
|
||||
"radio_initializing": radio_initializing,
|
||||
"radio_state": radio_state,
|
||||
"connection_info": connection_info,
|
||||
"radio_device_info": radio_device_info,
|
||||
"database_size_mb": db_size_mb,
|
||||
"oldest_undecrypted_timestamp": oldest_ts,
|
||||
"fanout_statuses": fanout_statuses,
|
||||
|
||||
@@ -7,6 +7,21 @@ POST_CONNECT_SETUP_TIMEOUT_SECONDS = 300
|
||||
POST_CONNECT_SETUP_MAX_ATTEMPTS = 2
|
||||
|
||||
|
||||
def _clean_device_string(value: object) -> str | None:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
cleaned = value.strip()
|
||||
return cleaned or None
|
||||
|
||||
|
||||
def _decode_fixed_string(raw: bytes, start: int, length: int) -> str | None:
|
||||
if len(raw) < start:
|
||||
return None
|
||||
return _clean_device_string(
|
||||
raw[start : start + length].decode("utf-8", "ignore").replace("\0", "")
|
||||
)
|
||||
|
||||
|
||||
async def run_post_connect_setup(radio_manager) -> None:
|
||||
"""Run shared radio initialization after a transport connection succeeds."""
|
||||
from app.event_handlers import register_event_handlers
|
||||
@@ -78,26 +93,66 @@ async def run_post_connect_setup(radio_manager) -> None:
|
||||
return await _original_handle_rx(data)
|
||||
|
||||
reader.handle_rx = _capture_handle_rx
|
||||
radio_manager.device_info_loaded = False
|
||||
radio_manager.max_contacts = None
|
||||
radio_manager.device_model = None
|
||||
radio_manager.firmware_build = None
|
||||
radio_manager.firmware_version = None
|
||||
radio_manager.max_channels = 40
|
||||
radio_manager.path_hash_mode = 0
|
||||
radio_manager.path_hash_mode_supported = False
|
||||
try:
|
||||
device_query = await mc.commands.send_device_query()
|
||||
if device_query and "max_channels" in device_query.payload:
|
||||
radio_manager.max_channels = max(
|
||||
1, int(device_query.payload["max_channels"])
|
||||
)
|
||||
if device_query and "path_hash_mode" in device_query.payload:
|
||||
radio_manager.path_hash_mode = device_query.payload["path_hash_mode"]
|
||||
payload = (
|
||||
device_query.payload
|
||||
if device_query is not None and isinstance(device_query.payload, dict)
|
||||
else {}
|
||||
)
|
||||
|
||||
payload_max_contacts = payload.get("max_contacts")
|
||||
if isinstance(payload_max_contacts, int):
|
||||
radio_manager.max_contacts = max(1, payload_max_contacts)
|
||||
|
||||
payload_max_channels = payload.get("max_channels")
|
||||
if isinstance(payload_max_channels, int):
|
||||
radio_manager.max_channels = max(1, payload_max_channels)
|
||||
|
||||
radio_manager.device_model = _clean_device_string(payload.get("model"))
|
||||
radio_manager.firmware_build = _clean_device_string(payload.get("fw_build"))
|
||||
radio_manager.firmware_version = _clean_device_string(payload.get("ver"))
|
||||
|
||||
fw_ver = payload.get("fw ver")
|
||||
payload_reports_device_info = isinstance(fw_ver, int) and fw_ver >= 3
|
||||
if payload_reports_device_info:
|
||||
radio_manager.device_info_loaded = True
|
||||
|
||||
if "path_hash_mode" in payload and isinstance(payload["path_hash_mode"], int):
|
||||
radio_manager.path_hash_mode = payload["path_hash_mode"]
|
||||
radio_manager.path_hash_mode_supported = True
|
||||
elif _captured_frame:
|
||||
# Raw-frame fallback:
|
||||
# byte 1 = fw_ver, byte 3 = max_channels, byte 81 = path_hash_mode
|
||||
|
||||
if _captured_frame:
|
||||
# Raw-frame fallback / completion:
|
||||
# byte 1 = fw_ver, byte 2 = max_contacts/2, byte 3 = max_channels,
|
||||
# bytes 8:20 = fw_build, 20:60 = model, 60:80 = ver, byte 81 = path_hash_mode
|
||||
raw = _captured_frame[-1]
|
||||
fw_ver = raw[1] if len(raw) > 1 else 0
|
||||
if fw_ver >= 3 and len(raw) >= 4:
|
||||
radio_manager.max_channels = max(1, raw[3])
|
||||
if fw_ver >= 10 and len(raw) >= 82:
|
||||
if fw_ver >= 3:
|
||||
radio_manager.device_info_loaded = True
|
||||
if radio_manager.max_contacts is None and len(raw) >= 3:
|
||||
radio_manager.max_contacts = max(1, raw[2] * 2)
|
||||
if len(raw) >= 4 and not isinstance(payload_max_channels, int):
|
||||
radio_manager.max_channels = max(1, raw[3])
|
||||
if radio_manager.firmware_build is None:
|
||||
radio_manager.firmware_build = _decode_fixed_string(raw, 8, 12)
|
||||
if radio_manager.device_model is None:
|
||||
radio_manager.device_model = _decode_fixed_string(raw, 20, 40)
|
||||
if radio_manager.firmware_version is None:
|
||||
radio_manager.firmware_version = _decode_fixed_string(raw, 60, 20)
|
||||
if (
|
||||
not radio_manager.path_hash_mode_supported
|
||||
and fw_ver >= 10
|
||||
and len(raw) >= 82
|
||||
):
|
||||
radio_manager.path_hash_mode = raw[81]
|
||||
radio_manager.path_hash_mode_supported = True
|
||||
logger.warning(
|
||||
@@ -114,6 +169,17 @@ async def run_post_connect_setup(radio_manager) -> None:
|
||||
logger.info("Path hash mode: %d (supported)", radio_manager.path_hash_mode)
|
||||
else:
|
||||
logger.debug("Firmware does not report path_hash_mode")
|
||||
if radio_manager.device_info_loaded:
|
||||
logger.info(
|
||||
"Radio device info: model=%s build=%s version=%s max_contacts=%s max_channels=%d",
|
||||
radio_manager.device_model or "unknown",
|
||||
radio_manager.firmware_build or "unknown",
|
||||
radio_manager.firmware_version or "unknown",
|
||||
radio_manager.max_contacts
|
||||
if radio_manager.max_contacts is not None
|
||||
else "unknown",
|
||||
radio_manager.max_channels,
|
||||
)
|
||||
logger.info("Max channel slots: %d", radio_manager.max_channels)
|
||||
except Exception as exc:
|
||||
logger.debug("Failed to query device info capabilities: %s", exc)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
import{r as u,D as j,j as e,U as y,m as N}from"./index-C_33NHxn.js";import{M as w,T as _}from"./leaflet-DNHXPpW7.js";import{C as k,P as C}from"./Popup-BcLg2SsA.js";import{u as M}from"./hooks-BmAJqTPK.js";const i={recent:"#06b6d4",today:"#2563eb",stale:"#f59e0b",old:"#64748b"},R="#0f172a",p="#f8fafc";function E(a){if(a==null)return i.old;const l=Date.now()/1e3-a,n=3600,t=86400;return l<n?i.recent:l<t?i.today:l<3*t?i.stale:i.old}function v({contacts:a,focusedContact:r}){const l=M(),[n,t]=u.useState(!1);return u.useEffect(()=>{if(r&&r.lat!=null&&r.lon!=null){l.setView([r.lat,r.lon],12),t(!0);return}if(n)return;const c=()=>{if(a.length===0){l.setView([20,0],2),t(!0);return}if(a.length===1){l.setView([a[0].lat,a[0].lon],10),t(!0);return}const d=a.map(m=>[m.lat,m.lon]);l.fitBounds(d,{padding:[50,50],maxZoom:12}),t(!0)};"geolocation"in navigator?navigator.geolocation.getCurrentPosition(d=>{l.setView([d.coords.latitude,d.coords.longitude],8),t(!0)},()=>{c()},{timeout:5e3,maximumAge:3e5}):c()},[l,a,n,r]),null}function V({contacts:a,focusedKey:r}){const[l]=u.useState(()=>Date.now()/1e3-604800),n=u.useMemo(()=>a.filter(s=>j(s.lat,s.lon)&&(s.public_key===r||s.last_seen!=null&&s.last_seen>l)),[a,r,l]),t=u.useMemo(()=>r&&n.find(s=>s.public_key===r)||null,[r,n]),c=t!=null&&(t.last_seen==null||t.last_seen<=l),d=u.useRef({}),m=u.useCallback((s,o)=>{d.current[s]=o},[]);return u.useEffect(()=>{if(t&&d.current[t.public_key]){const s=setTimeout(()=>{var o;(o=d.current[t.public_key])==null||o.openPopup()},100);return()=>clearTimeout(s)}},[t]),e.jsxs("div",{className:"flex flex-col h-full",children:[e.jsxs("div",{className:"px-4 py-2 bg-muted/50 text-xs text-muted-foreground flex items-center justify-between",children:[e.jsxs("span",{children:["Showing ",n.length," contact",n.length!==1?"s":""," heard in the last 7 days",c?" plus the focused contact":""]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.recent},"aria-hidden":"true"})," ","<1h"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.today},"aria-hidden":"true"})," ","<1d"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.stale},"aria-hidden":"true"})," ","<3d"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.old},"aria-hidden":"true"})," ","older"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full border-2",style:{borderColor:p,backgroundColor:i.today},"aria-hidden":"true"})," ","repeater"]})]})]}),e.jsx("div",{className:"flex-1 relative",style:{zIndex:0},role:"img","aria-label":"Map showing mesh node locations",children:e.jsxs(w,{center:[20,0],zoom:2,className:"h-full w-full",style:{background:"#1a1a2e"},children:[e.jsx(_,{attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),e.jsx(v,{contacts:n,focusedContact:t}),n.map(s=>{const o=s.type===y,x=E(s.last_seen),f=s.name||s.public_key.slice(0,12),h=s.last_seen!=null?N(s.last_seen):"Never heard by this server",g=o?10:7;return e.jsx(u.Fragment,{children:e.jsx(k,{ref:b=>m(s.public_key,b),center:[s.lat,s.lon],radius:g,pathOptions:{color:o?p:R,fillColor:x,fillOpacity:.9,weight:o?3:2},children:e.jsx(C,{children:e.jsxs("div",{className:"text-sm",children:[e.jsxs("div",{className:"font-medium flex items-center gap-1",children:[o&&e.jsx("span",{title:"Repeater","aria-hidden":"true",children:"🛜"}),f]}),e.jsxs("div",{className:"text-xs text-gray-500 mt-1",children:["Last heard: ",h]}),e.jsxs("div",{className:"text-xs text-gray-400 mt-1 font-mono",children:[s.lat.toFixed(5),", ",s.lon.toFixed(5)]})]})})},s.public_key)},s.public_key)})]})})]})}export{V as MapView};
|
||||
//# sourceMappingURL=MapView-B_EeV5Ur.js.map
|
||||
import{r as u,D as j,j as e,U as y,m as N}from"./index-D_aPHvl1.js";import{M as w,T as _}from"./leaflet-Kmmg5omM.js";import{C as k,P as C}from"./Popup-Sx9rYjUO.js";import{u as M}from"./hooks-DO7QgCnP.js";const i={recent:"#06b6d4",today:"#2563eb",stale:"#f59e0b",old:"#64748b"},R="#0f172a",p="#f8fafc";function E(a){if(a==null)return i.old;const l=Date.now()/1e3-a,n=3600,t=86400;return l<n?i.recent:l<t?i.today:l<3*t?i.stale:i.old}function v({contacts:a,focusedContact:r}){const l=M(),[n,t]=u.useState(!1);return u.useEffect(()=>{if(r&&r.lat!=null&&r.lon!=null){l.setView([r.lat,r.lon],12),t(!0);return}if(n)return;const c=()=>{if(a.length===0){l.setView([20,0],2),t(!0);return}if(a.length===1){l.setView([a[0].lat,a[0].lon],10),t(!0);return}const d=a.map(m=>[m.lat,m.lon]);l.fitBounds(d,{padding:[50,50],maxZoom:12}),t(!0)};"geolocation"in navigator?navigator.geolocation.getCurrentPosition(d=>{l.setView([d.coords.latitude,d.coords.longitude],8),t(!0)},()=>{c()},{timeout:5e3,maximumAge:3e5}):c()},[l,a,n,r]),null}function V({contacts:a,focusedKey:r}){const[l]=u.useState(()=>Date.now()/1e3-604800),n=u.useMemo(()=>a.filter(s=>j(s.lat,s.lon)&&(s.public_key===r||s.last_seen!=null&&s.last_seen>l)),[a,r,l]),t=u.useMemo(()=>r&&n.find(s=>s.public_key===r)||null,[r,n]),c=t!=null&&(t.last_seen==null||t.last_seen<=l),d=u.useRef({}),m=u.useCallback((s,o)=>{d.current[s]=o},[]);return u.useEffect(()=>{if(t&&d.current[t.public_key]){const s=setTimeout(()=>{var o;(o=d.current[t.public_key])==null||o.openPopup()},100);return()=>clearTimeout(s)}},[t]),e.jsxs("div",{className:"flex flex-col h-full",children:[e.jsxs("div",{className:"px-4 py-2 bg-muted/50 text-xs text-muted-foreground flex items-center justify-between",children:[e.jsxs("span",{children:["Showing ",n.length," contact",n.length!==1?"s":""," heard in the last 7 days",c?" plus the focused contact":""]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.recent},"aria-hidden":"true"})," ","<1h"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.today},"aria-hidden":"true"})," ","<1d"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.stale},"aria-hidden":"true"})," ","<3d"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.old},"aria-hidden":"true"})," ","older"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full border-2",style:{borderColor:p,backgroundColor:i.today},"aria-hidden":"true"})," ","repeater"]})]})]}),e.jsx("div",{className:"flex-1 relative",style:{zIndex:0},role:"img","aria-label":"Map showing mesh node locations",children:e.jsxs(w,{center:[20,0],zoom:2,className:"h-full w-full",style:{background:"#1a1a2e"},children:[e.jsx(_,{attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),e.jsx(v,{contacts:n,focusedContact:t}),n.map(s=>{const o=s.type===y,x=E(s.last_seen),f=s.name||s.public_key.slice(0,12),h=s.last_seen!=null?N(s.last_seen):"Never heard by this server",g=o?10:7;return e.jsx(u.Fragment,{children:e.jsx(k,{ref:b=>m(s.public_key,b),center:[s.lat,s.lon],radius:g,pathOptions:{color:o?p:R,fillColor:x,fillOpacity:.9,weight:o?3:2},children:e.jsx(C,{children:e.jsxs("div",{className:"text-sm",children:[e.jsxs("div",{className:"font-medium flex items-center gap-1",children:[o&&e.jsx("span",{title:"Repeater","aria-hidden":"true",children:"🛜"}),f]}),e.jsxs("div",{className:"text-xs text-gray-500 mt-1",children:["Last heard: ",h]}),e.jsxs("div",{className:"text-xs text-gray-400 mt-1 font-mono",children:[s.lat.toFixed(5),", ",s.lon.toFixed(5)]})]})})},s.public_key)},s.public_key)})]})})]})}export{V as MapView};
|
||||
//# sourceMappingURL=MapView-C3Y0Y3CX.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
import{j as l}from"./index-C_33NHxn.js";import{d as m,l as u,a as f,e as d,M as x,T as y}from"./leaflet-DNHXPpW7.js";import{C as p,P as c}from"./Popup-BcLg2SsA.js";const g=m(function({positions:n,...t},o){const s=new u.Polyline(n,t);return f(s,d(o,{overlayContainer:s}))},function(n,t,o){t.positions!==o.positions&&n.setLatLngs(t.positions)});function C({neighbors:i,radioLat:n,radioLon:t,radioName:o}){const s=i.filter(e=>e.lat!=null&&e.lon!=null),r=n!=null&&t!=null&&!(n===0&&t===0);if(s.length===0&&!r)return null;const h=r?[n,t]:[s[0].lat,s[0].lon];return l.jsx("div",{className:"min-h-48 flex-1 rounded border border-border overflow-hidden",role:"img","aria-label":"Map showing repeater neighbor locations",children:l.jsxs(x,{center:h,zoom:10,className:"h-full w-full",style:{background:"#1a1a2e"},children:[l.jsx(y,{attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),r&&s.map((e,a)=>l.jsx(g,{positions:[[n,t],[e.lat,e.lon]],pathOptions:{color:"#3b82f6",weight:1.5,opacity:.5,dashArray:"6 4"}},`line-${a}`)),r&&l.jsx(p,{center:[n,t],radius:8,pathOptions:{color:"#1d4ed8",fillColor:"#3b82f6",fillOpacity:1,weight:2},children:l.jsx(c,{children:l.jsx("span",{className:"text-sm font-medium",children:o||"Our Radio"})})}),s.map((e,a)=>l.jsx(p,{center:[e.lat,e.lon],radius:6,pathOptions:{color:"#000",fillColor:e.snr>=6?"#22c55e":e.snr>=0?"#eab308":"#ef4444",fillOpacity:.8,weight:1},children:l.jsx(c,{children:l.jsx("span",{className:"text-sm",children:e.name||e.pubkey_prefix})})},a))]})})}export{C as NeighborsMiniMap};
|
||||
//# sourceMappingURL=NeighborsMiniMap-CsH85D0E.js.map
|
||||
import{j as l}from"./index-D_aPHvl1.js";import{d as m,l as u,a as f,e as d,M as x,T as y}from"./leaflet-Kmmg5omM.js";import{C as p,P as c}from"./Popup-Sx9rYjUO.js";const g=m(function({positions:n,...t},o){const s=new u.Polyline(n,t);return f(s,d(o,{overlayContainer:s}))},function(n,t,o){t.positions!==o.positions&&n.setLatLngs(t.positions)});function C({neighbors:i,radioLat:n,radioLon:t,radioName:o}){const s=i.filter(e=>e.lat!=null&&e.lon!=null),r=n!=null&&t!=null&&!(n===0&&t===0);if(s.length===0&&!r)return null;const h=r?[n,t]:[s[0].lat,s[0].lon];return l.jsx("div",{className:"min-h-48 flex-1 rounded border border-border overflow-hidden",role:"img","aria-label":"Map showing repeater neighbor locations",children:l.jsxs(x,{center:h,zoom:10,className:"h-full w-full",style:{background:"#1a1a2e"},children:[l.jsx(y,{attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),r&&s.map((e,a)=>l.jsx(g,{positions:[[n,t],[e.lat,e.lon]],pathOptions:{color:"#3b82f6",weight:1.5,opacity:.5,dashArray:"6 4"}},`line-${a}`)),r&&l.jsx(p,{center:[n,t],radius:8,pathOptions:{color:"#1d4ed8",fillColor:"#3b82f6",fillOpacity:1,weight:2},children:l.jsx(c,{children:l.jsx("span",{className:"text-sm font-medium",children:o||"Our Radio"})})}),s.map((e,a)=>l.jsx(p,{center:[e.lat,e.lon],radius:6,pathOptions:{color:"#000",fillColor:e.snr>=6?"#22c55e":e.snr>=0?"#eab308":"#ef4444",fillOpacity:.8,weight:1},children:l.jsx(c,{children:l.jsx("span",{className:"text-sm",children:e.name||e.pubkey_prefix})})},a))]})})}export{C as NeighborsMiniMap};
|
||||
//# sourceMappingURL=NeighborsMiniMap-DJtDAXli.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import{r as x,D as l,j as i}from"./index-C_33NHxn.js";import{c as O,l as b,a as y,e as w,b as C,M as L,T as M,L as R}from"./leaflet-DNHXPpW7.js";import{u as T}from"./hooks-BmAJqTPK.js";const h=O(function({position:n,...t},o){const r=new b.Marker(n,t);return y(r,w(o,{overlayContainer:r}))},function(n,t,o){t.position!==o.position&&n.setLatLng(t.position),t.icon!=null&&t.icon!==o.icon&&n.setIcon(t.icon),t.zIndexOffset!=null&&t.zIndexOffset!==o.zIndexOffset&&n.setZIndexOffset(t.zIndexOffset),t.opacity!=null&&t.opacity!==o.opacity&&n.setOpacity(t.opacity),n.dragging!=null&&t.draggable!==o.draggable&&(t.draggable===!0?n.dragging.enable():n.dragging.disable())}),p=C(function(n,t){const o=new b.Tooltip(n,t.overlayContainer);return y(o,t)},function(n,t,{position:o},r){x.useEffect(function(){const s=t.overlayContainer;if(s==null)return;const{instance:u}=n,f=a=>{a.tooltip===u&&(o!=null&&u.setLatLng(o),u.update(),r(!0))},c=a=>{a.tooltip===u&&r(!1)};return s.on({tooltipopen:f,tooltipclose:c}),s.bindTooltip(u),function(){s.off({tooltipopen:f,tooltipclose:c}),s._map!=null&&s.unbindTooltip()}},[n,t,r,o])}),m=["#f97316","#eab308","#22c55e","#06b6d4","#ec4899","#f43f5e","#a855f7","#64748b"],E="#3b82f6",S="#8b5cf6";function g(e,n){return R.divIcon({className:"",iconSize:[24,24],iconAnchor:[12,12],html:`<div style="
|
||||
import{r as x,D as l,j as i}from"./index-D_aPHvl1.js";import{c as O,l as b,a as y,e as w,b as C,M as L,T as M,L as R}from"./leaflet-Kmmg5omM.js";import{u as T}from"./hooks-DO7QgCnP.js";const h=O(function({position:n,...t},o){const r=new b.Marker(n,t);return y(r,w(o,{overlayContainer:r}))},function(n,t,o){t.position!==o.position&&n.setLatLng(t.position),t.icon!=null&&t.icon!==o.icon&&n.setIcon(t.icon),t.zIndexOffset!=null&&t.zIndexOffset!==o.zIndexOffset&&n.setZIndexOffset(t.zIndexOffset),t.opacity!=null&&t.opacity!==o.opacity&&n.setOpacity(t.opacity),n.dragging!=null&&t.draggable!==o.draggable&&(t.draggable===!0?n.dragging.enable():n.dragging.disable())}),p=C(function(n,t){const o=new b.Tooltip(n,t.overlayContainer);return y(o,t)},function(n,t,{position:o},r){x.useEffect(function(){const s=t.overlayContainer;if(s==null)return;const{instance:u}=n,f=a=>{a.tooltip===u&&(o!=null&&u.setLatLng(o),u.update(),r(!0))},c=a=>{a.tooltip===u&&r(!1)};return s.on({tooltipopen:f,tooltipclose:c}),s.bindTooltip(u),function(){s.off({tooltipopen:f,tooltipclose:c}),s._map!=null&&s.unbindTooltip()}},[n,t,r,o])}),m=["#f97316","#eab308","#22c55e","#06b6d4","#ec4899","#f43f5e","#a855f7","#64748b"],E="#3b82f6",S="#8b5cf6";function g(e,n){return R.divIcon({className:"",iconSize:[24,24],iconAnchor:[12,12],html:`<div style="
|
||||
width:24px;height:24px;border-radius:50%;
|
||||
background:${n};color:#fff;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
@@ -6,4 +6,4 @@ import{r as x,D as l,j as i}from"./index-C_33NHxn.js";import{c as O,l as b,a as
|
||||
border:2px solid rgba(255,255,255,0.8);
|
||||
box-shadow:0 1px 4px rgba(0,0,0,0.4);
|
||||
">${e}</div>`})}function z(e){return m[e%m.length]}function N(e){const n=[];l(e.sender.lat,e.sender.lon)&&n.push([e.sender.lat,e.sender.lon]);for(const t of e.hops)for(const o of t.matches)l(o.lat,o.lon)&&n.push([o.lat,o.lon]);return l(e.receiver.lat,e.receiver.lon)&&n.push([e.receiver.lat,e.receiver.lon]),n}function I({points:e}){const n=T(),t=x.useRef(!1);return x.useEffect(()=>{t.current||e.length===0||(t.current=!0,e.length===1?n.setView(e[0],12):n.fitBounds(e,{padding:[30,30],maxZoom:14}))},[n,e]),null}function k({resolved:e,senderInfo:n}){const t=N(e),o=t.length>0;let r=2,d=0;l(e.sender.lat,e.sender.lon)&&d++,l(e.receiver.lat,e.receiver.lon)&&d++;for(const f of e.hops)f.matches.length===0?r++:(r+=f.matches.length,d+=f.matches.filter(c=>l(c.lat,c.lon)).length);const s=o&&d<r;if(!o)return i.jsx("div",{className:"h-14 rounded border border-border bg-muted/30 flex items-center justify-center text-sm text-muted-foreground",children:"No nodes in this route have GPS coordinates"});const u=t[0];return i.jsxs("div",{children:[i.jsx("div",{className:"rounded border border-border overflow-hidden",role:"img","aria-label":"Map showing message route between nodes",style:{height:220},children:i.jsxs(L,{center:u,zoom:10,className:"h-full w-full",style:{background:"#1a1a2e"},children:[i.jsx(M,{attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),i.jsx(I,{points:t}),l(e.sender.lat,e.sender.lon)&&i.jsx(h,{position:[e.sender.lat,e.sender.lon],icon:g("S",E),children:i.jsx(p,{direction:"top",offset:[0,-14],children:n.name||"Sender"})}),e.hops.map((f,c)=>f.matches.filter(a=>l(a.lat,a.lon)).map((a,j)=>i.jsx(h,{position:[a.lat,a.lon],icon:g(String(c+1),z(c)),children:i.jsx(p,{direction:"top",offset:[0,-14],children:a.name||a.public_key.slice(0,12)})},`hop-${c}-${j}`))),l(e.receiver.lat,e.receiver.lon)&&i.jsx(h,{position:[e.receiver.lat,e.receiver.lon],icon:g("R",S),children:i.jsx(p,{direction:"top",offset:[0,-14],children:e.receiver.name||"Receiver"})})]})}),s&&i.jsx("p",{className:"text-xs text-muted-foreground mt-1",children:"Some nodes in this route have no GPS and are not shown"})]})}export{k as PathRouteMap};
|
||||
//# sourceMappingURL=PathRouteMap-DbzDNGyG.js.map
|
||||
//# sourceMappingURL=PathRouteMap-CJMPJrMX.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
import{d,l as f,a as s,e as C,b as m}from"./leaflet-DNHXPpW7.js";import{r as P}from"./index-C_33NHxn.js";function v(o,n,e){n.center!==e.center&&o.setLatLng(n.center),n.radius!=null&&n.radius!==e.radius&&o.setRadius(n.radius)}const b=d(function({center:n,children:e,...r},p){const t=new f.CircleMarker(n,r);return s(t,C(p,{overlayContainer:t}))},v),k=m(function(n,e){const r=new f.Popup(n,e.overlayContainer);return s(r,e)},function(n,e,{position:r},p){P.useEffect(function(){const{instance:a}=n;function i(u){u.popup===a&&(a.update(),p(!0))}function c(u){u.popup===a&&p(!1)}return e.map.on({popupopen:i,popupclose:c}),e.overlayContainer==null?(r!=null&&a.setLatLng(r),a.openOn(e.map)):e.overlayContainer.bindPopup(a),function(){var l;e.map.off({popupopen:i,popupclose:c}),(l=e.overlayContainer)==null||l.unbindPopup(),e.map.removeLayer(a)}},[n,e,p,r])});export{b as C,k as P};
|
||||
//# sourceMappingURL=Popup-BcLg2SsA.js.map
|
||||
import{d,l as f,a as s,e as C,b as m}from"./leaflet-Kmmg5omM.js";import{r as P}from"./index-D_aPHvl1.js";function v(o,n,e){n.center!==e.center&&o.setLatLng(n.center),n.radius!=null&&n.radius!==e.radius&&o.setRadius(n.radius)}const b=d(function({center:n,children:e,...r},p){const t=new f.CircleMarker(n,r);return s(t,C(p,{overlayContainer:t}))},v),k=m(function(n,e){const r=new f.Popup(n,e.overlayContainer);return s(r,e)},function(n,e,{position:r},p){P.useEffect(function(){const{instance:a}=n;function i(u){u.popup===a&&(a.update(),p(!0))}function c(u){u.popup===a&&p(!1)}return e.map.on({popupopen:i,popupclose:c}),e.overlayContainer==null?(r!=null&&a.setLatLng(r),a.openOn(e.map)):e.overlayContainer.bindPopup(a),function(){var l;e.map.off({popupopen:i,popupclose:c}),(l=e.overlayContainer)==null||l.unbindPopup(),e.map.removeLayer(a)}},[n,e,p,r])});export{b as C,k as P};
|
||||
//# sourceMappingURL=Popup-Sx9rYjUO.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"Popup-BcLg2SsA.js","sources":["../../node_modules/@react-leaflet/core/lib/circle.js","../../node_modules/react-leaflet/lib/CircleMarker.js","../../node_modules/react-leaflet/lib/Popup.js"],"sourcesContent":["export function updateCircle(layer, props, prevProps) {\n if (props.center !== prevProps.center) {\n layer.setLatLng(props.center);\n }\n if (props.radius != null && props.radius !== prevProps.radius) {\n layer.setRadius(props.radius);\n }\n}\n","import { createElementObject, createPathComponent, extendContext, updateCircle } from '@react-leaflet/core';\nimport { CircleMarker as LeafletCircleMarker } from 'leaflet';\nexport const CircleMarker = createPathComponent(function createCircleMarker({ center , children: _c , ...options }, ctx) {\n const marker = new LeafletCircleMarker(center, options);\n return createElementObject(marker, extendContext(ctx, {\n overlayContainer: marker\n }));\n}, updateCircle);\n","import { createElementObject, createOverlayComponent } from '@react-leaflet/core';\nimport { Popup as LeafletPopup } from 'leaflet';\nimport { useEffect } from 'react';\nexport const Popup = createOverlayComponent(function createPopup(props, context) {\n const popup = new LeafletPopup(props, context.overlayContainer);\n return createElementObject(popup, context);\n}, function usePopupLifecycle(element, context, { position }, setOpen) {\n useEffect(function addPopup() {\n const { instance } = element;\n function onPopupOpen(event) {\n if (event.popup === instance) {\n instance.update();\n setOpen(true);\n }\n }\n function onPopupClose(event) {\n if (event.popup === instance) {\n setOpen(false);\n }\n }\n context.map.on({\n popupopen: onPopupOpen,\n popupclose: onPopupClose\n });\n if (context.overlayContainer == null) {\n // Attach to a Map\n if (position != null) {\n instance.setLatLng(position);\n }\n instance.openOn(context.map);\n } else {\n // Attach to container component\n context.overlayContainer.bindPopup(instance);\n }\n return function removePopup() {\n context.map.off({\n popupopen: onPopupOpen,\n popupclose: onPopupClose\n });\n context.overlayContainer?.unbindPopup();\n context.map.removeLayer(instance);\n };\n }, [\n element,\n context,\n setOpen,\n position\n ]);\n});\n"],"names":["updateCircle","layer","props","prevProps","CircleMarker","createPathComponent","center","_c","options","ctx","marker","LeafletCircleMarker","createElementObject","extendContext","Popup","createOverlayComponent","context","popup","LeafletPopup","element","position","setOpen","useEffect","instance","onPopupOpen","event","onPopupClose","_a"],"mappings":"yGAAO,SAASA,EAAaC,EAAOC,EAAOC,EAAW,CAC9CD,EAAM,SAAWC,EAAU,QAC3BF,EAAM,UAAUC,EAAM,MAAM,EAE5BA,EAAM,QAAU,MAAQA,EAAM,SAAWC,EAAU,QACnDF,EAAM,UAAUC,EAAM,MAAM,CAEpC,CCLY,MAACE,EAAeC,EAAoB,SAA4B,CAAE,OAAAC,EAAS,SAAUC,EAAK,GAAGC,CAAO,EAAIC,EAAK,CACrH,MAAMC,EAAS,IAAIC,eAAoBL,EAAQE,CAAO,EACtD,OAAOI,EAAoBF,EAAQG,EAAcJ,EAAK,CAClD,iBAAkBC,CAC1B,CAAK,CAAC,CACN,EAAGV,CAAY,ECJFc,EAAQC,EAAuB,SAAqBb,EAAOc,EAAS,CAC7E,MAAMC,EAAQ,IAAIC,EAAAA,MAAahB,EAAOc,EAAQ,gBAAgB,EAC9D,OAAOJ,EAAoBK,EAAOD,CAAO,CAC7C,EAAG,SAA2BG,EAASH,EAAS,CAAE,SAAAI,CAAQ,EAAKC,EAAS,CACpEC,EAAAA,UAAU,UAAoB,CAC1B,KAAM,CAAE,SAAAC,CAAQ,EAAMJ,EACtB,SAASK,EAAYC,EAAO,CACpBA,EAAM,QAAUF,IAChBA,EAAS,OAAM,EACfF,EAAQ,EAAI,EAEpB,CACA,SAASK,EAAaD,EAAO,CACrBA,EAAM,QAAUF,GAChBF,EAAQ,EAAK,CAErB,CACA,OAAAL,EAAQ,IAAI,GAAG,CACX,UAAWQ,EACX,WAAYE,CACxB,CAAS,EACGV,EAAQ,kBAAoB,MAExBI,GAAY,MACZG,EAAS,UAAUH,CAAQ,EAE/BG,EAAS,OAAOP,EAAQ,GAAG,GAG3BA,EAAQ,iBAAiB,UAAUO,CAAQ,EAExC,UAAuB,OAC1BP,EAAQ,IAAI,IAAI,CACZ,UAAWQ,EACX,WAAYE,CAC5B,CAAa,GACDC,EAAAX,EAAQ,mBAAR,MAAAW,EAA0B,cAC1BX,EAAQ,IAAI,YAAYO,CAAQ,CACpC,CACJ,EAAG,CACCJ,EACAH,EACAK,EACAD,CACR,CAAK,CACL,CAAC","x_google_ignoreList":[0,1,2]}
|
||||
{"version":3,"file":"Popup-Sx9rYjUO.js","sources":["../../node_modules/@react-leaflet/core/lib/circle.js","../../node_modules/react-leaflet/lib/CircleMarker.js","../../node_modules/react-leaflet/lib/Popup.js"],"sourcesContent":["export function updateCircle(layer, props, prevProps) {\n if (props.center !== prevProps.center) {\n layer.setLatLng(props.center);\n }\n if (props.radius != null && props.radius !== prevProps.radius) {\n layer.setRadius(props.radius);\n }\n}\n","import { createElementObject, createPathComponent, extendContext, updateCircle } from '@react-leaflet/core';\nimport { CircleMarker as LeafletCircleMarker } from 'leaflet';\nexport const CircleMarker = createPathComponent(function createCircleMarker({ center , children: _c , ...options }, ctx) {\n const marker = new LeafletCircleMarker(center, options);\n return createElementObject(marker, extendContext(ctx, {\n overlayContainer: marker\n }));\n}, updateCircle);\n","import { createElementObject, createOverlayComponent } from '@react-leaflet/core';\nimport { Popup as LeafletPopup } from 'leaflet';\nimport { useEffect } from 'react';\nexport const Popup = createOverlayComponent(function createPopup(props, context) {\n const popup = new LeafletPopup(props, context.overlayContainer);\n return createElementObject(popup, context);\n}, function usePopupLifecycle(element, context, { position }, setOpen) {\n useEffect(function addPopup() {\n const { instance } = element;\n function onPopupOpen(event) {\n if (event.popup === instance) {\n instance.update();\n setOpen(true);\n }\n }\n function onPopupClose(event) {\n if (event.popup === instance) {\n setOpen(false);\n }\n }\n context.map.on({\n popupopen: onPopupOpen,\n popupclose: onPopupClose\n });\n if (context.overlayContainer == null) {\n // Attach to a Map\n if (position != null) {\n instance.setLatLng(position);\n }\n instance.openOn(context.map);\n } else {\n // Attach to container component\n context.overlayContainer.bindPopup(instance);\n }\n return function removePopup() {\n context.map.off({\n popupopen: onPopupOpen,\n popupclose: onPopupClose\n });\n context.overlayContainer?.unbindPopup();\n context.map.removeLayer(instance);\n };\n }, [\n element,\n context,\n setOpen,\n position\n ]);\n});\n"],"names":["updateCircle","layer","props","prevProps","CircleMarker","createPathComponent","center","_c","options","ctx","marker","LeafletCircleMarker","createElementObject","extendContext","Popup","createOverlayComponent","context","popup","LeafletPopup","element","position","setOpen","useEffect","instance","onPopupOpen","event","onPopupClose","_a"],"mappings":"yGAAO,SAASA,EAAaC,EAAOC,EAAOC,EAAW,CAC9CD,EAAM,SAAWC,EAAU,QAC3BF,EAAM,UAAUC,EAAM,MAAM,EAE5BA,EAAM,QAAU,MAAQA,EAAM,SAAWC,EAAU,QACnDF,EAAM,UAAUC,EAAM,MAAM,CAEpC,CCLY,MAACE,EAAeC,EAAoB,SAA4B,CAAE,OAAAC,EAAS,SAAUC,EAAK,GAAGC,CAAO,EAAIC,EAAK,CACrH,MAAMC,EAAS,IAAIC,eAAoBL,EAAQE,CAAO,EACtD,OAAOI,EAAoBF,EAAQG,EAAcJ,EAAK,CAClD,iBAAkBC,CAC1B,CAAK,CAAC,CACN,EAAGV,CAAY,ECJFc,EAAQC,EAAuB,SAAqBb,EAAOc,EAAS,CAC7E,MAAMC,EAAQ,IAAIC,EAAAA,MAAahB,EAAOc,EAAQ,gBAAgB,EAC9D,OAAOJ,EAAoBK,EAAOD,CAAO,CAC7C,EAAG,SAA2BG,EAASH,EAAS,CAAE,SAAAI,CAAQ,EAAKC,EAAS,CACpEC,EAAAA,UAAU,UAAoB,CAC1B,KAAM,CAAE,SAAAC,CAAQ,EAAMJ,EACtB,SAASK,EAAYC,EAAO,CACpBA,EAAM,QAAUF,IAChBA,EAAS,OAAM,EACfF,EAAQ,EAAI,EAEpB,CACA,SAASK,EAAaD,EAAO,CACrBA,EAAM,QAAUF,GAChBF,EAAQ,EAAK,CAErB,CACA,OAAAL,EAAQ,IAAI,GAAG,CACX,UAAWQ,EACX,WAAYE,CACxB,CAAS,EACGV,EAAQ,kBAAoB,MAExBI,GAAY,MACZG,EAAS,UAAUH,CAAQ,EAE/BG,EAAS,OAAOP,EAAQ,GAAG,GAG3BA,EAAQ,iBAAiB,UAAUO,CAAQ,EAExC,UAAuB,OAC1BP,EAAQ,IAAI,IAAI,CACZ,UAAWQ,EACX,WAAYE,CAC5B,CAAa,GACDC,EAAAX,EAAQ,mBAAR,MAAAW,EAA0B,cAC1BX,EAAQ,IAAI,YAAYO,CAAQ,CACpC,CACJ,EAAG,CACCJ,EACAH,EACAK,EACAD,CACR,CAAK,CACL,CAAC","x_google_ignoreList":[0,1,2]}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
50
frontend/prebuilt/assets/SettingsModal-BaQM9_V7.js
Normal file
50
frontend/prebuilt/assets/SettingsModal-BaQM9_V7.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/prebuilt/assets/SettingsModal-BaQM9_V7.js.map
Normal file
1
frontend/prebuilt/assets/SettingsModal-BaQM9_V7.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
import"./index-C_33NHxn.js";import{u as t}from"./leaflet-DNHXPpW7.js";function r(){return t().map}export{r as u};
|
||||
//# sourceMappingURL=hooks-BmAJqTPK.js.map
|
||||
2
frontend/prebuilt/assets/hooks-DO7QgCnP.js
Normal file
2
frontend/prebuilt/assets/hooks-DO7QgCnP.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import"./index-D_aPHvl1.js";import{u as t}from"./leaflet-Kmmg5omM.js";function r(){return t().map}export{r as u};
|
||||
//# sourceMappingURL=hooks-DO7QgCnP.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"hooks-BmAJqTPK.js","sources":["../../node_modules/react-leaflet/lib/hooks.js"],"sourcesContent":["import { useLeafletContext } from '@react-leaflet/core';\nimport { useEffect } from 'react';\nexport function useMap() {\n return useLeafletContext().map;\n}\nexport function useMapEvent(type, handler) {\n const map = useMap();\n useEffect(function addMapEventHandler() {\n // @ts-ignore event type\n map.on(type, handler);\n return function removeMapEventHandler() {\n // @ts-ignore event type\n map.off(type, handler);\n };\n }, [\n map,\n type,\n handler\n ]);\n return map;\n}\nexport function useMapEvents(handlers) {\n const map = useMap();\n useEffect(function addMapEventHandlers() {\n map.on(handlers);\n return function removeMapEventHandlers() {\n map.off(handlers);\n };\n }, [\n map,\n handlers\n ]);\n return map;\n}\n"],"names":["useMap","useLeafletContext"],"mappings":"sEAEO,SAASA,GAAS,CACrB,OAAOC,EAAiB,EAAG,GAC/B","x_google_ignoreList":[0]}
|
||||
{"version":3,"file":"hooks-DO7QgCnP.js","sources":["../../node_modules/react-leaflet/lib/hooks.js"],"sourcesContent":["import { useLeafletContext } from '@react-leaflet/core';\nimport { useEffect } from 'react';\nexport function useMap() {\n return useLeafletContext().map;\n}\nexport function useMapEvent(type, handler) {\n const map = useMap();\n useEffect(function addMapEventHandler() {\n // @ts-ignore event type\n map.on(type, handler);\n return function removeMapEventHandler() {\n // @ts-ignore event type\n map.off(type, handler);\n };\n }, [\n map,\n type,\n handler\n ]);\n return map;\n}\nexport function useMapEvents(handlers) {\n const map = useMap();\n useEffect(function addMapEventHandlers() {\n map.on(handlers);\n return function removeMapEventHandlers() {\n map.off(handlers);\n };\n }, [\n map,\n handlers\n ]);\n return map;\n}\n"],"names":["useMap","useLeafletContext"],"mappings":"sEAEO,SAASA,GAAS,CACrB,OAAOC,EAAiB,EAAG,GAC/B","x_google_ignoreList":[0]}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
import{r as s,j as l,Q as f,l as u}from"./index-C_33NHxn.js";var N=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],h=N.reduce((a,r)=>{const t=f(`Primitive.${r}`),o=s.forwardRef((i,e)=>{const{asChild:p,...n}=i,m=p?t:r;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),l.jsx(m,{...n,ref:e})});return o.displayName=`Primitive.${r}`,{...a,[r]:o}},{}),x="Separator",c="horizontal",w=["horizontal","vertical"],d=s.forwardRef((a,r)=>{const{decorative:t,orientation:o=c,...i}=a,e=S(o)?o:c,n=t?{role:"none"}:{"aria-orientation":e==="vertical"?e:void 0,role:"separator"};return l.jsx(h.div,{"data-orientation":e,...n,...i,ref:r})});d.displayName=x;function S(a){return w.includes(a)}var v=d;const O=s.forwardRef(({className:a,orientation:r="horizontal",decorative:t=!0,...o},i)=>l.jsx(v,{ref:i,decorative:t,orientation:r,className:u("shrink-0 bg-border",r==="horizontal"?"h-[1px] w-full":"h-full w-[1px]",a),...o}));O.displayName=v.displayName;export{O as S};
|
||||
//# sourceMappingURL=separator-BtL4plOj.js.map
|
||||
import{r as s,j as l,Q as f,l as u}from"./index-D_aPHvl1.js";var N=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],h=N.reduce((a,r)=>{const t=f(`Primitive.${r}`),o=s.forwardRef((i,e)=>{const{asChild:p,...n}=i,m=p?t:r;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),l.jsx(m,{...n,ref:e})});return o.displayName=`Primitive.${r}`,{...a,[r]:o}},{}),x="Separator",c="horizontal",w=["horizontal","vertical"],d=s.forwardRef((a,r)=>{const{decorative:t,orientation:o=c,...i}=a,e=S(o)?o:c,n=t?{role:"none"}:{"aria-orientation":e==="vertical"?e:void 0,role:"separator"};return l.jsx(h.div,{"data-orientation":e,...n,...i,ref:r})});d.displayName=x;function S(a){return w.includes(a)}var v=d;const O=s.forwardRef(({className:a,orientation:r="horizontal",decorative:t=!0,...o},i)=>l.jsx(v,{ref:i,decorative:t,orientation:r,className:u("shrink-0 bg-border",r==="horizontal"?"h-[1px] w-full":"h-full w-[1px]",a),...o}));O.displayName=v.displayName;export{O as S};
|
||||
//# sourceMappingURL=separator-DL1oR9lA.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -50,7 +50,7 @@
|
||||
undecryptedCount: fetchJsonOrThrow('/api/packets/undecrypted/count'),
|
||||
};
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-C_33NHxn.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-D_aPHvl1.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cx5ENk0V.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -333,6 +333,35 @@ export function SettingsRadioSection({
|
||||
? `Connection paused${health?.connection_info ? ` (${health.connection_info})` : ''}`
|
||||
: 'Not connected';
|
||||
|
||||
const deviceInfoLabel = useMemo(() => {
|
||||
const info = health?.radio_device_info;
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const model = info.model?.trim() || null;
|
||||
const firmwareParts = [info.firmware_build?.trim(), info.firmware_version?.trim()].filter(
|
||||
(value): value is string => Boolean(value)
|
||||
);
|
||||
const capacityParts = [
|
||||
typeof info.max_contacts === 'number' ? `${info.max_contacts} contacts` : null,
|
||||
typeof info.max_channels === 'number' ? `${info.max_channels} channels` : null,
|
||||
].filter((value): value is string => value !== null);
|
||||
|
||||
if (!model && firmwareParts.length === 0 && capacityParts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let label = model ?? 'Radio';
|
||||
if (firmwareParts.length > 0) {
|
||||
label += ` running ${firmwareParts.join('/')}`;
|
||||
}
|
||||
if (capacityParts.length > 0) {
|
||||
label += ` (max: ${capacityParts.join(', ')})`;
|
||||
}
|
||||
return label;
|
||||
}, [health?.radio_device_info]);
|
||||
|
||||
const handleConnectionAction = async () => {
|
||||
setConnectionBusy(true);
|
||||
try {
|
||||
@@ -377,6 +406,7 @@ export function SettingsRadioSection({
|
||||
{connectionStatusLabel}
|
||||
</span>
|
||||
</div>
|
||||
{deviceInfoLabel && <p className="text-sm text-muted-foreground">{deviceInfoLabel}</p>}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -205,6 +205,26 @@ describe('SettingsModal', () => {
|
||||
expect(screen.getByText(/Configured radio contact capacity/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows cached radio firmware and capacity info under the connection status', () => {
|
||||
renderModal({
|
||||
health: {
|
||||
...baseHealth,
|
||||
radio_device_info: {
|
||||
model: 'T-Echo',
|
||||
firmware_build: '2025-02-01',
|
||||
firmware_version: '1.2.3',
|
||||
max_contacts: 350,
|
||||
max_channels: 64,
|
||||
},
|
||||
},
|
||||
});
|
||||
openRadioSection();
|
||||
|
||||
expect(
|
||||
screen.getByText('T-Echo running 2025-02-01/1.2.3 (max: 350 contacts, 64 channels)')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows reconnect action when radio connection is paused', () => {
|
||||
renderModal({
|
||||
health: { ...baseHealth, radio_state: 'paused' },
|
||||
|
||||
@@ -57,6 +57,13 @@ export interface HealthStatus {
|
||||
radio_initializing: boolean;
|
||||
radio_state?: 'connected' | 'initializing' | 'connecting' | 'disconnected' | 'paused';
|
||||
connection_info: string | null;
|
||||
radio_device_info?: {
|
||||
model: string | null;
|
||||
firmware_build: string | null;
|
||||
firmware_version: string | null;
|
||||
max_contacts: number | null;
|
||||
max_channels: number | null;
|
||||
} | null;
|
||||
database_size_mb: number;
|
||||
oldest_undecrypted_timestamp: number | null;
|
||||
fanout_statuses: Record<string, FanoutStatusEntry>;
|
||||
|
||||
@@ -78,6 +78,11 @@ class TestHealthEndpoint:
|
||||
with patch("app.routers.health.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
|
||||
mock_rm.is_setup_in_progress = False
|
||||
mock_rm.is_setup_complete = True
|
||||
mock_rm.connection_desired = True
|
||||
mock_rm.is_reconnecting = False
|
||||
mock_rm.device_info_loaded = False
|
||||
|
||||
from app.main import app
|
||||
|
||||
@@ -97,6 +102,11 @@ class TestHealthEndpoint:
|
||||
with patch("app.routers.health.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.connection_info = None
|
||||
mock_rm.is_setup_in_progress = False
|
||||
mock_rm.is_setup_complete = False
|
||||
mock_rm.connection_desired = True
|
||||
mock_rm.is_reconnecting = False
|
||||
mock_rm.device_info_loaded = False
|
||||
|
||||
from app.main import app
|
||||
|
||||
@@ -1118,6 +1128,11 @@ class TestHealthEndpointDatabaseSize:
|
||||
):
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
|
||||
mock_rm.is_setup_in_progress = False
|
||||
mock_rm.is_setup_complete = True
|
||||
mock_rm.connection_desired = True
|
||||
mock_rm.is_reconnecting = False
|
||||
mock_rm.device_info_loaded = False
|
||||
mock_getsize.return_value = 10 * 1024 * 1024 # 10 MB
|
||||
|
||||
from app.main import app
|
||||
@@ -1148,6 +1163,11 @@ class TestHealthEndpointOldestUndecrypted:
|
||||
):
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
|
||||
mock_rm.is_setup_in_progress = False
|
||||
mock_rm.is_setup_complete = True
|
||||
mock_rm.connection_desired = True
|
||||
mock_rm.is_reconnecting = False
|
||||
mock_rm.device_info_loaded = False
|
||||
mock_getsize.return_value = 5 * 1024 * 1024 # 5 MB
|
||||
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=1700000000)
|
||||
|
||||
@@ -1175,6 +1195,11 @@ class TestHealthEndpointOldestUndecrypted:
|
||||
):
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
|
||||
mock_rm.is_setup_in_progress = False
|
||||
mock_rm.is_setup_complete = True
|
||||
mock_rm.connection_desired = True
|
||||
mock_rm.is_reconnecting = False
|
||||
mock_rm.device_info_loaded = False
|
||||
mock_getsize.return_value = 1 * 1024 * 1024 # 1 MB
|
||||
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
|
||||
|
||||
@@ -1202,6 +1227,11 @@ class TestHealthEndpointOldestUndecrypted:
|
||||
):
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.connection_info = None
|
||||
mock_rm.is_setup_in_progress = False
|
||||
mock_rm.is_setup_complete = False
|
||||
mock_rm.connection_desired = True
|
||||
mock_rm.is_reconnecting = False
|
||||
mock_rm.device_info_loaded = False
|
||||
mock_getsize.side_effect = OSError("File not found")
|
||||
mock_repo.get_oldest_undecrypted = AsyncMock(side_effect=RuntimeError("No DB"))
|
||||
|
||||
|
||||
@@ -59,6 +59,35 @@ class TestHealthFanoutStatus:
|
||||
assert data["radio_state"] == "connected"
|
||||
assert data["connection_info"] == "Serial: /dev/ttyUSB0"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_includes_cached_radio_device_info(self, test_db):
|
||||
"""Health includes device metadata captured during post-connect setup."""
|
||||
with (
|
||||
patch(
|
||||
"app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None
|
||||
),
|
||||
patch("app.routers.health.radio_manager") as mock_rm,
|
||||
):
|
||||
mock_rm.is_setup_in_progress = False
|
||||
mock_rm.is_setup_complete = True
|
||||
mock_rm.connection_desired = True
|
||||
mock_rm.is_reconnecting = False
|
||||
mock_rm.device_info_loaded = True
|
||||
mock_rm.device_model = "T-Echo"
|
||||
mock_rm.firmware_build = "2025-02-01"
|
||||
mock_rm.firmware_version = "1.2.3"
|
||||
mock_rm.max_contacts = 350
|
||||
mock_rm.max_channels = 64
|
||||
data = await build_health_data(True, "Serial: /dev/ttyUSB0")
|
||||
|
||||
assert data["radio_device_info"] == {
|
||||
"model": "T-Echo",
|
||||
"firmware_build": "2025-02-01",
|
||||
"firmware_version": "1.2.3",
|
||||
"max_contacts": 350,
|
||||
"max_channels": 64,
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_status_degraded_when_disconnected(self, test_db):
|
||||
"""Health status is 'degraded' when radio is disconnected."""
|
||||
|
||||
@@ -476,6 +476,11 @@ class TestManualDisconnectCleanup:
|
||||
mock_mc.connection_manager = connection_manager
|
||||
rm._meshcore = mock_mc
|
||||
rm._setup_complete = True
|
||||
rm.device_info_loaded = True
|
||||
rm.max_contacts = 350
|
||||
rm.device_model = "T-Echo"
|
||||
rm.firmware_build = "2025-02-01"
|
||||
rm.firmware_version = "1.2.3"
|
||||
rm.max_channels = 8
|
||||
rm.path_hash_mode = 2
|
||||
rm.path_hash_mode_supported = True
|
||||
@@ -491,6 +496,11 @@ class TestManualDisconnectCleanup:
|
||||
assert reconnect_task is not None and reconnect_task.cancelled()
|
||||
assert rm.meshcore is None
|
||||
assert rm.is_setup_complete is False
|
||||
assert rm.device_info_loaded is False
|
||||
assert rm.max_contacts is None
|
||||
assert rm.device_model is None
|
||||
assert rm.firmware_build is None
|
||||
assert rm.firmware_version is None
|
||||
assert rm.max_channels == 40
|
||||
assert rm.path_hash_mode == 0
|
||||
assert rm.path_hash_mode_supported is False
|
||||
|
||||
@@ -112,6 +112,11 @@ class TestRunPostConnectSetup:
|
||||
radio_manager._setup_lock = None
|
||||
radio_manager._setup_in_progress = False
|
||||
radio_manager._setup_complete = False
|
||||
radio_manager.device_info_loaded = False
|
||||
radio_manager.max_contacts = None
|
||||
radio_manager.device_model = None
|
||||
radio_manager.firmware_build = None
|
||||
radio_manager.firmware_version = None
|
||||
radio_manager.max_channels = 40
|
||||
radio_manager.path_hash_mode = 0
|
||||
radio_manager.path_hash_mode_supported = False
|
||||
@@ -145,3 +150,66 @@ class TestRunPostConnectSetup:
|
||||
replacement_mc.start_auto_message_fetching.assert_awaited_once()
|
||||
initial_mc.start_auto_message_fetching.assert_not_called()
|
||||
assert radio_manager.max_channels == 8
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_caches_device_info_metadata_from_device_query(self):
|
||||
mc = MagicMock()
|
||||
mc.commands.send_device_query = AsyncMock(
|
||||
return_value=MagicMock(
|
||||
payload={
|
||||
"fw ver": 10,
|
||||
"max_contacts": 350,
|
||||
"max_channels": 64,
|
||||
"model": "T-Echo",
|
||||
"fw_build": "2025-02-01",
|
||||
"ver": "1.2.3",
|
||||
"path_hash_mode": 2,
|
||||
}
|
||||
)
|
||||
)
|
||||
mc.commands.set_flood_scope = AsyncMock(return_value=None)
|
||||
mc._reader = MagicMock()
|
||||
mc._reader.handle_rx = AsyncMock()
|
||||
mc.start_auto_message_fetching = AsyncMock()
|
||||
|
||||
radio_manager = MagicMock()
|
||||
radio_manager.meshcore = mc
|
||||
radio_manager._setup_lock = None
|
||||
radio_manager._setup_in_progress = False
|
||||
radio_manager._setup_complete = False
|
||||
radio_manager.device_info_loaded = False
|
||||
radio_manager.max_contacts = None
|
||||
radio_manager.device_model = None
|
||||
radio_manager.firmware_build = None
|
||||
radio_manager.firmware_version = None
|
||||
radio_manager.max_channels = 40
|
||||
radio_manager.path_hash_mode = 0
|
||||
radio_manager.path_hash_mode_supported = False
|
||||
radio_manager._acquire_operation_lock = AsyncMock()
|
||||
radio_manager._release_operation_lock = MagicMock()
|
||||
|
||||
with (
|
||||
patch("app.event_handlers.register_event_handlers"),
|
||||
patch("app.keystore.export_and_store_private_key", new=AsyncMock()),
|
||||
patch("app.radio_sync.sync_radio_time", new=AsyncMock()),
|
||||
patch(
|
||||
"app.repository.AppSettingsRepository.get",
|
||||
new=AsyncMock(return_value=MagicMock(flood_scope=None)),
|
||||
),
|
||||
patch("app.radio_sync.sync_and_offload_all", new=AsyncMock(return_value={"synced": 0})),
|
||||
patch("app.radio_sync.send_advertisement", new=AsyncMock(return_value=False)),
|
||||
patch("app.radio_sync.drain_pending_messages", new=AsyncMock(return_value=0)),
|
||||
patch("app.radio_sync.start_periodic_sync"),
|
||||
patch("app.radio_sync.start_periodic_advert"),
|
||||
patch("app.radio_sync.start_message_polling"),
|
||||
):
|
||||
await run_post_connect_setup(radio_manager)
|
||||
|
||||
assert radio_manager.device_info_loaded is True
|
||||
assert radio_manager.max_contacts == 350
|
||||
assert radio_manager.max_channels == 64
|
||||
assert radio_manager.device_model == "T-Echo"
|
||||
assert radio_manager.firmware_build == "2025-02-01"
|
||||
assert radio_manager.firmware_version == "1.2.3"
|
||||
assert radio_manager.path_hash_mode == 2
|
||||
assert radio_manager.path_hash_mode_supported is True
|
||||
|
||||
@@ -41,6 +41,11 @@ class TestWebSocketEndpoint:
|
||||
mock_ws_rm.connection_info = "Serial: /dev/ttyUSB0"
|
||||
mock_health_rm.is_connected = True
|
||||
mock_health_rm.connection_info = "Serial: /dev/ttyUSB0"
|
||||
mock_health_rm.is_setup_in_progress = False
|
||||
mock_health_rm.is_setup_complete = True
|
||||
mock_health_rm.connection_desired = True
|
||||
mock_health_rm.is_reconnecting = False
|
||||
mock_health_rm.device_info_loaded = False
|
||||
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
|
||||
mock_settings.database_path = "/tmp/test.db"
|
||||
mock_settings.disable_bots = False
|
||||
@@ -75,6 +80,11 @@ class TestWebSocketEndpoint:
|
||||
mock_ws_rm.connection_info = None
|
||||
mock_health_rm.is_connected = False
|
||||
mock_health_rm.connection_info = None
|
||||
mock_health_rm.is_setup_in_progress = False
|
||||
mock_health_rm.is_setup_complete = False
|
||||
mock_health_rm.connection_desired = True
|
||||
mock_health_rm.is_reconnecting = False
|
||||
mock_health_rm.device_info_loaded = False
|
||||
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
|
||||
mock_settings.database_path = "/tmp/test.db"
|
||||
mock_settings.disable_bots = False
|
||||
@@ -105,6 +115,11 @@ class TestWebSocketEndpoint:
|
||||
mock_ws_rm.connection_info = "TCP: 192.168.1.1:4000"
|
||||
mock_health_rm.is_connected = True
|
||||
mock_health_rm.connection_info = "TCP: 192.168.1.1:4000"
|
||||
mock_health_rm.is_setup_in_progress = False
|
||||
mock_health_rm.is_setup_complete = True
|
||||
mock_health_rm.connection_desired = True
|
||||
mock_health_rm.is_reconnecting = False
|
||||
mock_health_rm.device_info_loaded = False
|
||||
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
|
||||
mock_settings.database_path = "/tmp/test.db"
|
||||
mock_settings.disable_bots = False
|
||||
@@ -136,6 +151,11 @@ class TestWebSocketEndpoint:
|
||||
mock_ws_rm.connection_info = "Serial: /dev/ttyUSB0"
|
||||
mock_health_rm.is_connected = True
|
||||
mock_health_rm.connection_info = "Serial: /dev/ttyUSB0"
|
||||
mock_health_rm.is_setup_in_progress = False
|
||||
mock_health_rm.is_setup_complete = True
|
||||
mock_health_rm.connection_desired = True
|
||||
mock_health_rm.is_reconnecting = False
|
||||
mock_health_rm.device_info_loaded = False
|
||||
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
|
||||
mock_settings.database_path = "/tmp/test.db"
|
||||
mock_settings.disable_bots = False
|
||||
@@ -167,6 +187,11 @@ class TestWebSocketEndpoint:
|
||||
mock_ws_rm.connection_info = "Serial: /dev/ttyUSB0"
|
||||
mock_health_rm.is_connected = True
|
||||
mock_health_rm.connection_info = "Serial: /dev/ttyUSB0"
|
||||
mock_health_rm.is_setup_in_progress = False
|
||||
mock_health_rm.is_setup_complete = True
|
||||
mock_health_rm.connection_desired = True
|
||||
mock_health_rm.is_reconnecting = False
|
||||
mock_health_rm.device_info_loaded = False
|
||||
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
|
||||
mock_settings.database_path = "/tmp/test.db"
|
||||
mock_settings.disable_bots = False
|
||||
|
||||
Reference in New Issue
Block a user