mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Fix typo and add better ops for decrypt API calls
This commit is contained in:
@@ -3,7 +3,7 @@ from hashlib import sha256
|
||||
from sqlite3 import OperationalError
|
||||
|
||||
import aiosqlite
|
||||
from fastapi import APIRouter, BackgroundTasks
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Response, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.database import db
|
||||
@@ -40,6 +40,10 @@ class DecryptResult(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
def _bad_request(detail: str) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
|
||||
|
||||
|
||||
async def _run_historical_channel_decryption(
|
||||
channel_key_bytes: bytes, channel_key_hex: str, display_name: str | None = None
|
||||
) -> None:
|
||||
@@ -100,7 +104,7 @@ async def get_undecrypted_count() -> dict:
|
||||
|
||||
@router.post("/decrypt/historical", response_model=DecryptResult)
|
||||
async def decrypt_historical_packets(
|
||||
request: DecryptRequest, background_tasks: BackgroundTasks
|
||||
request: DecryptRequest, background_tasks: BackgroundTasks, response: Response
|
||||
) -> DecryptResult:
|
||||
"""
|
||||
Attempt to decrypt historical packets with the provided key.
|
||||
@@ -112,27 +116,15 @@ async def decrypt_historical_packets(
|
||||
try:
|
||||
channel_key_bytes = bytes.fromhex(request.channel_key)
|
||||
if len(channel_key_bytes) != 16:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Channel key must be 16 bytes (32 hex chars)",
|
||||
)
|
||||
raise _bad_request("Channel key must be 16 bytes (32 hex chars)")
|
||||
channel_key_hex = request.channel_key.upper()
|
||||
except ValueError:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Invalid hex string for channel key",
|
||||
)
|
||||
raise _bad_request("Invalid hex string for channel key") from None
|
||||
elif request.channel_name:
|
||||
channel_key_bytes = sha256(request.channel_name.encode("utf-8")).digest()[:16]
|
||||
channel_key_hex = channel_key_bytes.hex().upper()
|
||||
else:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Must provide channel_key or channel_name",
|
||||
)
|
||||
raise _bad_request("Must provide channel_key or channel_name")
|
||||
|
||||
# Get count and lookup channel name for display
|
||||
count = await RawPacketRepository.get_undecrypted_count()
|
||||
@@ -148,6 +140,7 @@ async def decrypt_historical_packets(
|
||||
background_tasks.add_task(
|
||||
_run_historical_channel_decryption, channel_key_bytes, channel_key_hex, display_name
|
||||
)
|
||||
response.status_code = status.HTTP_202_ACCEPTED
|
||||
|
||||
return DecryptResult(
|
||||
started=True,
|
||||
@@ -158,48 +151,24 @@ async def decrypt_historical_packets(
|
||||
elif request.key_type == "contact":
|
||||
# DM decryption
|
||||
if not request.private_key:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Must provide private_key for contact decryption",
|
||||
)
|
||||
raise _bad_request("Must provide private_key for contact decryption")
|
||||
if not request.contact_public_key:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Must provide contact_public_key for contact decryption",
|
||||
)
|
||||
raise _bad_request("Must provide contact_public_key for contact decryption")
|
||||
|
||||
try:
|
||||
private_key_bytes = bytes.fromhex(request.private_key)
|
||||
if len(private_key_bytes) != 64:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Private key must be 64 bytes (128 hex chars)",
|
||||
)
|
||||
raise _bad_request("Private key must be 64 bytes (128 hex chars)")
|
||||
except ValueError:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Invalid hex string for private key",
|
||||
)
|
||||
raise _bad_request("Invalid hex string for private key") from None
|
||||
|
||||
try:
|
||||
contact_public_key_bytes = bytes.fromhex(request.contact_public_key)
|
||||
if len(contact_public_key_bytes) != 32:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Contact public key must be 32 bytes (64 hex chars)",
|
||||
)
|
||||
raise _bad_request("Contact public key must be 32 bytes (64 hex chars)")
|
||||
contact_public_key_hex = request.contact_public_key.lower()
|
||||
except ValueError:
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="Invalid hex string for contact public key",
|
||||
)
|
||||
raise _bad_request("Invalid hex string for contact public key") from None
|
||||
|
||||
packets = await RawPacketRepository.get_undecrypted_text_messages()
|
||||
count = len(packets)
|
||||
@@ -223,6 +192,7 @@ async def decrypt_historical_packets(
|
||||
contact_public_key_hex,
|
||||
display_name,
|
||||
)
|
||||
response.status_code = status.HTTP_202_ACCEPTED
|
||||
|
||||
return DecryptResult(
|
||||
started=True,
|
||||
@@ -230,11 +200,7 @@ async def decrypt_historical_packets(
|
||||
message=f"Started DM decryption of {count} TEXT_MESSAGE packets in background",
|
||||
)
|
||||
|
||||
return DecryptResult(
|
||||
started=False,
|
||||
total_packets=0,
|
||||
message="key_type must be 'channel' or 'contact'",
|
||||
)
|
||||
raise _bad_request("key_type must be 'channel' or 'contact'")
|
||||
|
||||
|
||||
class MaintenanceRequest(BaseModel):
|
||||
|
||||
@@ -415,6 +415,10 @@ PYTHONPATH=. uv run pytest tests/ -v
|
||||
|
||||
## Errata & Known Non-Issues
|
||||
|
||||
### Contacts rollup uses mention styling for unread DMs
|
||||
|
||||
This is intentional. In the sidebar section headers, unread direct messages are treated as mention-equivalent, so the Contacts rollup uses the highlighted mention-style badge for any unread DM. Row-level mention detection remains separate; this note is only about the section summary styling.
|
||||
|
||||
### RawPacketList always scrolls to bottom
|
||||
|
||||
`RawPacketList` unconditionally scrolls to the latest packet on every update. This is intentional — the packet feed is a live status display, not an interactive log meant for lingering or long-term analysis. Users watching it want to see the newest packet, not hold a scroll position.
|
||||
|
||||
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 d,D as j,j as e,U as y,m as N}from"./index-BJ3L5G2s.js";import{M as w,T as _}from"./leaflet-TNk2YzRY.js";import{C as k,P as C}from"./Popup-JtfOLvZE.js";import{u as M}from"./hooks-NBTgSuxX.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 r=Date.now()/1e3-a,n=3600,l=86400;return r<n?i.recent:r<l?i.today:r<3*l?i.stale:i.old}function v({contacts:a,focusedContact:t}){const r=M(),[n,l]=d.useState(!1);return d.useEffect(()=>{if(t&&t.lat!=null&&t.lon!=null){r.setView([t.lat,t.lon],12),l(!0);return}if(n)return;const c=()=>{if(a.length===0){r.setView([20,0],2),l(!0);return}if(a.length===1){r.setView([a[0].lat,a[0].lon],10),l(!0);return}const u=a.map(m=>[m.lat,m.lon]);r.fitBounds(u,{padding:[50,50],maxZoom:12}),l(!0)};"geolocation"in navigator?navigator.geolocation.getCurrentPosition(u=>{r.setView([u.coords.latitude,u.coords.longitude],8),l(!0)},()=>{c()},{timeout:5e3,maximumAge:3e5}):c()},[r,a,n,t]),null}function V({contacts:a,focusedKey:t}){const r=Date.now()/1e3-604800,n=d.useMemo(()=>a.filter(s=>j(s.lat,s.lon)&&(s.public_key===t||s.last_seen!=null&&s.last_seen>r)),[a,t,r]),l=d.useMemo(()=>t&&n.find(s=>s.public_key===t)||null,[t,n]),c=l!=null&&(l.last_seen==null||l.last_seen<=r),u=d.useRef({}),m=d.useCallback((s,o)=>{u.current[s]=o},[]);return d.useEffect(()=>{if(l&&u.current[l.public_key]){const s=setTimeout(()=>{var o;(o=u.current[l.public_key])==null||o.openPopup()},100);return()=>clearTimeout(s)}},[l]),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:l}),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(d.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-DG71Do9A.js.map
|
||||
import{r as d,D as j,j as e,U as y,m as N}from"./index-D8to1fXL.js";import{M as w,T as _}from"./leaflet-CQVZ2keI.js";import{C as k,P as C}from"./Popup-65Kx45OI.js";import{u as M}from"./hooks-BHXvHkvm.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 r=Date.now()/1e3-a,n=3600,l=86400;return r<n?i.recent:r<l?i.today:r<3*l?i.stale:i.old}function v({contacts:a,focusedContact:t}){const r=M(),[n,l]=d.useState(!1);return d.useEffect(()=>{if(t&&t.lat!=null&&t.lon!=null){r.setView([t.lat,t.lon],12),l(!0);return}if(n)return;const c=()=>{if(a.length===0){r.setView([20,0],2),l(!0);return}if(a.length===1){r.setView([a[0].lat,a[0].lon],10),l(!0);return}const u=a.map(m=>[m.lat,m.lon]);r.fitBounds(u,{padding:[50,50],maxZoom:12}),l(!0)};"geolocation"in navigator?navigator.geolocation.getCurrentPosition(u=>{r.setView([u.coords.latitude,u.coords.longitude],8),l(!0)},()=>{c()},{timeout:5e3,maximumAge:3e5}):c()},[r,a,n,t]),null}function V({contacts:a,focusedKey:t}){const r=Date.now()/1e3-604800,n=d.useMemo(()=>a.filter(s=>j(s.lat,s.lon)&&(s.public_key===t||s.last_seen!=null&&s.last_seen>r)),[a,t,r]),l=d.useMemo(()=>t&&n.find(s=>s.public_key===t)||null,[t,n]),c=l!=null&&(l.last_seen==null||l.last_seen<=r),u=d.useRef({}),m=d.useCallback((s,o)=>{u.current[s]=o},[]);return d.useEffect(()=>{if(l&&u.current[l.public_key]){const s=setTimeout(()=>{var o;(o=u.current[l.public_key])==null||o.openPopup()},100);return()=>clearTimeout(s)}},[l]),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:l}),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(d.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-CgY0ZIub.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
import{j as l}from"./index-BJ3L5G2s.js";import{d as m,l as u,a as f,e as d,M as x,T as y}from"./leaflet-TNk2YzRY.js";import{C as p,P as c}from"./Popup-JtfOLvZE.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-D6bvxuvI.js.map
|
||||
import{j as l}from"./index-D8to1fXL.js";import{d as m,l as u,a as f,e as d,M as x,T as y}from"./leaflet-CQVZ2keI.js";import{C as p,P as c}from"./Popup-65Kx45OI.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-nhWnbDjS.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-BJ3L5G2s.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-TNk2YzRY.js";import{u as T}from"./hooks-NBTgSuxX.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-D8to1fXL.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-CQVZ2keI.js";import{u as T}from"./hooks-BHXvHkvm.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-BJ3L5G2s.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-DiGw6Qka.js.map
|
||||
//# sourceMappingURL=PathRouteMap-BgANOQRu.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-TNk2YzRY.js";import{r as P}from"./index-BJ3L5G2s.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-JtfOLvZE.js.map
|
||||
import{d,l as f,a as s,e as C,b as m}from"./leaflet-CQVZ2keI.js";import{r as P}from"./index-D8to1fXL.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-65Kx45OI.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"Popup-JtfOLvZE.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-65Kx45OI.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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
frontend/prebuilt/assets/hooks-BHXvHkvm.js
Normal file
2
frontend/prebuilt/assets/hooks-BHXvHkvm.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import"./index-D8to1fXL.js";import{u as t}from"./leaflet-CQVZ2keI.js";function r(){return t().map}export{r as u};
|
||||
//# sourceMappingURL=hooks-BHXvHkvm.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"hooks-NBTgSuxX.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-BHXvHkvm.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]}
|
||||
@@ -1,2 +0,0 @@
|
||||
import"./index-BJ3L5G2s.js";import{u as t}from"./leaflet-TNk2YzRY.js";function r(){return t().map}export{r as u};
|
||||
//# sourceMappingURL=hooks-NBTgSuxX.js.map
|
||||
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-BJ3L5G2s.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-ChUIbJIJ.js.map
|
||||
import{r as s,j as l,Q as f,l as u}from"./index-D8to1fXL.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-CqKVy-fj.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-BJ3L5G2s.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-D8to1fXL.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Kkb-Bio9.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -304,7 +304,7 @@ export function ChatHeader({
|
||||
title={
|
||||
activeContactIsPrefixOnly
|
||||
? 'Direct Trace unavailable until the full contact key is known'
|
||||
: 'Direct Trace. Send a zero-hop packet to thie contact and display out and back SNR'
|
||||
: 'Direct Trace. Send a zero-hop packet to this contact and display out and back SNR'
|
||||
}
|
||||
aria-label="Direct Trace"
|
||||
disabled={activeContactIsPrefixOnly}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import { Button } from './ui/button';
|
||||
import { toast } from './ui/sonner';
|
||||
|
||||
type Tab = 'existing' | 'new-contact' | 'new-room' | 'hashtag';
|
||||
|
||||
@@ -90,6 +91,9 @@ export function NewMessageModal({
|
||||
resetForm();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
toast.error('Failed to create conversation', {
|
||||
description: err instanceof Error ? err.message : undefined,
|
||||
});
|
||||
setError(err instanceof Error ? err.message : 'Failed to create');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -123,6 +127,9 @@ export function NewMessageModal({
|
||||
setName('');
|
||||
hashtagInputRef.current?.focus();
|
||||
} catch (err) {
|
||||
toast.error('Failed to create conversation', {
|
||||
description: err instanceof Error ? err.message : undefined,
|
||||
});
|
||||
setError(err instanceof Error ? err.message : 'Failed to create');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
import { NewMessageModal } from '../components/NewMessageModal';
|
||||
import type { Contact } from '../types';
|
||||
import { toast } from '../components/ui/sonner';
|
||||
|
||||
// Mock sonner (toast)
|
||||
vi.mock('../components/ui/sonner', () => ({
|
||||
@@ -35,6 +36,11 @@ const mockContact: Contact = {
|
||||
first_seen: null,
|
||||
};
|
||||
|
||||
const mockToast = toast as unknown as {
|
||||
success: ReturnType<typeof vi.fn>;
|
||||
error: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
describe('NewMessageModal form reset', () => {
|
||||
const onClose = vi.fn();
|
||||
const onSelectConversation = vi.fn();
|
||||
@@ -137,6 +143,24 @@ describe('NewMessageModal form reset', () => {
|
||||
});
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('toasts when creation fails', async () => {
|
||||
const user = userEvent.setup();
|
||||
onCreateChannel.mockRejectedValueOnce(new Error('Bad key'));
|
||||
renderModal();
|
||||
await switchToTab(user, 'Room');
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Room name'), 'MyRoom');
|
||||
await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'cc'.repeat(16));
|
||||
await user.click(screen.getByRole('button', { name: 'Create' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToast.error).toHaveBeenCalledWith('Failed to create conversation', {
|
||||
description: 'Bad key',
|
||||
});
|
||||
});
|
||||
expect(screen.getByText('Bad key')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tab switching resets form', () => {
|
||||
|
||||
@@ -72,7 +72,7 @@ class TestDecryptHistoricalPackets:
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert data["started"] is True
|
||||
assert data["total_packets"] == 5
|
||||
@@ -91,7 +91,7 @@ class TestDecryptHistoricalPackets:
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert data["started"] is True
|
||||
assert data["total_packets"] == 3
|
||||
@@ -107,10 +107,9 @@ class TestDecryptHistoricalPackets:
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["started"] is False
|
||||
assert "invalid" in data["message"].lower()
|
||||
assert "invalid" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_channel_decrypt_wrong_key_length(self, test_db, client):
|
||||
@@ -123,10 +122,9 @@ class TestDecryptHistoricalPackets:
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["started"] is False
|
||||
assert "16 bytes" in data["message"]
|
||||
assert "16 bytes" in data["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_channel_decrypt_no_key_or_name(self, test_db, client):
|
||||
@@ -136,10 +134,9 @@ class TestDecryptHistoricalPackets:
|
||||
json={"key_type": "channel"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["started"] is False
|
||||
assert "must provide" in data["message"].lower()
|
||||
assert "must provide" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_channel_decrypt_no_undecrypted_packets(self, test_db, client):
|
||||
@@ -172,7 +169,7 @@ class TestDecryptHistoricalPackets:
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 202
|
||||
assert response.json()["started"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -186,10 +183,9 @@ class TestDecryptHistoricalPackets:
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["started"] is False
|
||||
assert "private_key" in data["message"].lower()
|
||||
assert "private_key" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_contact_decrypt_missing_contact_key(self, test_db, client):
|
||||
@@ -202,10 +198,9 @@ class TestDecryptHistoricalPackets:
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["started"] is False
|
||||
assert "contact_public_key" in data["message"].lower()
|
||||
assert "contact_public_key" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_contact_decrypt_wrong_private_key_length(self, test_db, client):
|
||||
@@ -219,10 +214,9 @@ class TestDecryptHistoricalPackets:
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["started"] is False
|
||||
assert "64 bytes" in data["message"]
|
||||
assert "64 bytes" in data["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_contact_decrypt_wrong_public_key_length(self, test_db, client):
|
||||
@@ -236,10 +230,9 @@ class TestDecryptHistoricalPackets:
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["started"] is False
|
||||
assert "32 bytes" in data["message"]
|
||||
assert "32 bytes" in data["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_contact_decrypt_invalid_hex(self, test_db, client):
|
||||
@@ -253,10 +246,9 @@ class TestDecryptHistoricalPackets:
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["started"] is False
|
||||
assert "invalid" in data["message"].lower()
|
||||
assert "invalid" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_key_type(self, test_db, client):
|
||||
@@ -266,10 +258,9 @@ class TestDecryptHistoricalPackets:
|
||||
json={"key_type": "invalid"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["started"] is False
|
||||
assert "key_type" in data["message"].lower()
|
||||
assert "key_type" in data["detail"].lower()
|
||||
|
||||
|
||||
class TestRunHistoricalChannelDecryption:
|
||||
|
||||
Reference in New Issue
Block a user