Move to multi-connection modality

This commit is contained in:
Jack Kingsman authored and jkingsman committed 2026-02-04 14:46:41 -08:00
1 parent 64d261e6f9
commit a86d2d7cda
23 files changed
+427 -63

No files matched your search

+8 -2
View File
@@ -20,7 +20,7 @@ npm run build # run a frontend build
## Overview
A web interface for MeshCore mesh radio networks. The backend connects to a MeshCore-compatible radio over serial and exposes REST/WebSocket APIs. The React frontend provides real-time messaging and radio configuration.
A web interface for MeshCore mesh radio networks. The backend connects to a MeshCore-compatible radio over Serial, TCP, or BLE and exposes REST/WebSocket APIs. The React frontend provides real-time messaging and radio configuration.
**For detailed component documentation, see:**
- `app/AGENTS.md` - Backend (FastAPI, database, radio connection, packet decryption)
@@ -54,7 +54,7 @@ A web interface for MeshCore mesh radio networks. The backend connects to a Mesh
│ │ RadioManager + Event Handlers │ │
│ └──────────────────────────────────────────────────────────┘ │
└───────────────────────────┼──────────────────────────────────────┘
│ Serial
│ Serial / TCP / BLE
┌──────┴──────┐
│ MeshCore │
│ Radio │
@@ -308,5 +308,11 @@ mc.subscribe(EventType.ACK, handler)
| Variable | Default | Description |
|----------|---------|-------------|
| `MESHCORE_SERIAL_PORT` | auto-detect | Serial port for radio |
| `MESHCORE_TCP_HOST` | *(none)* | TCP host for radio (mutually exclusive with serial/BLE) |
| `MESHCORE_TCP_PORT` | `4000` | TCP port (used with `MESHCORE_TCP_HOST`) |
| `MESHCORE_BLE_ADDRESS` | *(none)* | BLE device address (mutually exclusive with serial/TCP) |
| `MESHCORE_BLE_PIN` | *(required with BLE)* | BLE PIN code |
| `MESHCORE_DATABASE_PATH` | `data/meshcore.db` | SQLite database location |
| `MESHCORE_MAX_RADIO_CONTACTS` | `200` | Max recent contacts to keep on radio for DM ACKs |
**Transport mutual exclusivity:** Only one of `MESHCORE_SERIAL_PORT`, `MESHCORE_TCP_HOST`, or `MESHCORE_BLE_ADDRESS` may be set. If none are set, serial auto-detection is used.
+32 -6
View File
@@ -1,6 +1,6 @@
# RemoteTerm for MeshCore
Backend server + browser interface for MeshCore mesh radio networks. Attach your radio over serial, and then you can:
Backend server + browser interface for MeshCore mesh radio networks. Connect your radio over Serial, TCP, or BLE, and then you can:
* Send and receive DMs and GroupTexts
* Cache all received packets, decrypting as you gain keys
@@ -23,9 +23,11 @@ If extending, have your LLM read the three `AGENTS.md` files: `./AGENTS.md`, `./
- Python 3.10+
- Node.js 18+ (for frontend development only)
- [UV](https://astral.sh/uv) package manager: `curl -LsSf https://astral.sh/uv/install.sh | sh`
- MeshCore radio connected via USB serial
- MeshCore radio connected via USB serial, TCP, or BLE
<details>
<summary>Finding your serial port</summary>
**Find your serial port:**
```bash
#######
# Linux
@@ -50,8 +52,8 @@ usbipd list
# attach device to WSL
usbipd bind --busid 3-8
```
</details>
## Quick Start
@@ -67,9 +69,16 @@ uv sync
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
```
The server auto-detects the serial port. To specify manually:
The server auto-detects the serial port. To specify a transport manually:
```bash
# Serial (explicit port)
MESHCORE_SERIAL_PORT=/dev/ttyUSB0 uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
# TCP (e.g. via wifi-enabled firmware)
MESHCORE_TCP_HOST=192.168.1.100 MESHCORE_TCP_PORT=4000 uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
# BLE (address and PIN both required)
MESHCORE_BLE_ADDRESS=AA:BB:CC:DD:EE:FF MESHCORE_BLE_PIN=123456 uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
```
Access at http://localhost:8000
@@ -80,12 +89,23 @@ Access at http://localhost:8000
> **Warning:** Docker has intermittent issues with serial event subscriptions. The native method above is more reliable.
> **Note:** BLE-in-docker is outside the scope of this README, but the env vars should all still work.
```bash
# Serial
docker run -d \
--device=/dev/ttyUSB0 \
-v remoteterm-data:/app/data \
-p 8000:8000 \
jkingsman/remote-terminal-for-meshcore:latest
# TCP
docker run -d \
-e MESHCORE_TCP_HOST=192.168.1.100 \
-e MESHCORE_TCP_PORT=4000 \
-v remoteterm-data:/app/data \
-p 8000:8000 \
jkingsman/remote-terminal-for-meshcore:latest
```
## Development
@@ -139,11 +159,17 @@ npm run build # build the frontend
| Variable | Default | Description |
|----------|---------|-------------|
| `MESHCORE_SERIAL_PORT` | (auto-detect) | Serial port path |
| `MESHCORE_SERIAL_BAUDRATE` | 115200 | Baud rate |
| `MESHCORE_SERIAL_BAUDRATE` | 115200 | Serial baud rate |
| `MESHCORE_TCP_HOST` | | TCP host (mutually exclusive with serial/BLE) |
| `MESHCORE_TCP_PORT` | 4000 | TCP port |
| `MESHCORE_BLE_ADDRESS` | | BLE device address (mutually exclusive with serial/TCP) |
| `MESHCORE_BLE_PIN` | | BLE PIN (required when BLE address is set) |
| `MESHCORE_LOG_LEVEL` | INFO | DEBUG, INFO, WARNING, ERROR |
| `MESHCORE_DATABASE_PATH` | data/meshcore.db | SQLite database path |
| `MESHCORE_MAX_RADIO_CONTACTS` | 200 | Max recent contacts to keep on radio for DM ACKs |
Only one transport may be active at a time. If multiple are set, the server will refuse to start.
## Additional Setup
<details>
+7 -4
View File
@@ -62,7 +62,7 @@ await AppSettingsRepository.add_favorite("contact", public_key)
### Radio Connection
`RadioManager` in `radio.py` handles serial connection:
`RadioManager` in `radio.py` handles radio connection over Serial, TCP, or BLE:
```python
from app.radio import radio_manager
@@ -70,9 +70,12 @@ from app.radio import radio_manager
# Access meshcore instance
if radio_manager.meshcore:
await radio_manager.meshcore.commands.send_msg(dst, msg)
# Check connection info (e.g. "Serial: /dev/ttyUSB0", "TCP: 192.168.1.1:4000", "BLE: AA:BB:CC:DD:EE:FF")
print(radio_manager.connection_info)
```
Auto-detection scans common serial ports when `MESHCORE_SERIAL_PORT` is not set.
Transport is configured via env vars (see root AGENTS.md). When no transport env vars are set, serial auto-detection is used.
### Event-Driven Architecture
@@ -116,7 +119,7 @@ from app.websocket import broadcast_error, broadcast_health
broadcast_error("Operation failed", "Additional details")
# Notify clients of connection status change
broadcast_health(radio_connected=True, serial_port="/dev/ttyUSB0")
broadcast_health(radio_connected=True, connection_info="Serial: /dev/ttyUSB0")
```
### Connection Monitoring
@@ -464,7 +467,7 @@ result = await sync_recent_contacts_to_radio(force=True)
All endpoints are prefixed with `/api`.
### Health
- `GET /api/health` - Connection status, serial port
- `GET /api/health` - Connection status, connection info
### Radio
- `GET /api/radio/config` - Read config (public key, name, radio params)
+31
View File
@@ -1,6 +1,7 @@
import logging
from typing import Literal
from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -9,9 +10,39 @@ class Settings(BaseSettings):
serial_port: str = "" # Empty string triggers auto-detection
serial_baudrate: int = 115200
tcp_host: str = ""
tcp_port: int = 4000
ble_address: str = ""
ble_pin: str = ""
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
database_path: str = "data/meshcore.db"
@model_validator(mode="after")
def validate_transport_exclusivity(self) -> "Settings":
transports_set = sum(
[
bool(self.serial_port),
bool(self.tcp_host),
bool(self.ble_address),
]
)
if transports_set > 1:
raise ValueError(
"Only one transport may be configured at a time. "
"Set exactly one of MESHCORE_SERIAL_PORT, MESHCORE_TCP_HOST, or MESHCORE_BLE_ADDRESS."
)
if self.ble_address and not self.ble_pin:
raise ValueError("MESHCORE_BLE_PIN is required when MESHCORE_BLE_ADDRESS is set.")
return self
@property
def connection_type(self) -> Literal["serial", "tcp", "ble"]:
if self.tcp_host:
return "tcp"
if self.ble_address:
return "ble"
return "serial"
settings = Settings()
+51 -9
View File
@@ -102,7 +102,7 @@ class RadioManager:
def __init__(self):
self._meshcore: MeshCore | None = None
self._port: str | None = None
self._connection_info: str | None = None
self._reconnect_task: asyncio.Task | None = None
self._last_connected: bool = False
self._reconnect_lock: asyncio.Lock | None = None
@@ -169,8 +169,8 @@ class RadioManager:
return self._meshcore
@property
def port(self) -> str | None:
return self._port
def connection_info(self) -> str | None:
return self._connection_info
@property
def is_connected(self) -> bool:
@@ -181,10 +181,20 @@ class RadioManager:
return self._reconnect_lock is not None and self._reconnect_lock.locked()
async def connect(self) -> None:
"""Connect to the radio over serial."""
"""Connect to the radio using the configured transport."""
if self._meshcore is not None:
await self.disconnect()
connection_type = settings.connection_type
if connection_type == "tcp":
await self._connect_tcp()
elif connection_type == "ble":
await self._connect_ble()
else:
await self._connect_serial()
async def _connect_serial(self) -> None:
"""Connect to the radio over serial."""
port = settings.serial_port
# Auto-detect if no port specified
@@ -205,10 +215,42 @@ class RadioManager:
auto_reconnect=True,
max_reconnect_attempts=10,
)
self._port = port
self._connection_info = f"Serial: {port}"
self._last_connected = True
logger.debug("Serial connection established")
async def _connect_tcp(self) -> None:
"""Connect to the radio over TCP."""
host = settings.tcp_host
port = settings.tcp_port
logger.debug("Connecting to radio at %s:%d (TCP)", host, port)
self._meshcore = await MeshCore.create_tcp(
host=host,
port=port,
auto_reconnect=True,
max_reconnect_attempts=10,
)
self._connection_info = f"TCP: {host}:{port}"
self._last_connected = True
logger.debug("TCP connection established")
async def _connect_ble(self) -> None:
"""Connect to the radio over BLE."""
address = settings.ble_address
pin = settings.ble_pin
logger.debug("Connecting to radio at %s (BLE)", address)
self._meshcore = await MeshCore.create_ble(
address=address,
pin=pin,
auto_reconnect=True,
max_reconnect_attempts=15,
)
self._connection_info = f"BLE: {address}"
self._last_connected = True
logger.debug("BLE connection established")
async def disconnect(self) -> None:
"""Disconnect from the radio."""
if self._meshcore is not None:
@@ -250,8 +292,8 @@ class RadioManager:
await self.connect()
if self.is_connected:
logger.info("Radio reconnected successfully at %s", self._port)
broadcast_health(True, self._port)
logger.info("Radio reconnected successfully at %s", self._connection_info)
broadcast_health(True, self._connection_info)
return True
else:
logger.warning("Reconnection failed: not connected after connect()")
@@ -280,7 +322,7 @@ class RadioManager:
if self._last_connected and not current_connected:
# Connection lost
logger.warning("Radio connection lost, broadcasting status change")
broadcast_health(False, self._port)
broadcast_health(False, self._connection_info)
self._last_connected = False
# Attempt reconnection
@@ -291,7 +333,7 @@ class RadioManager:
elif not self._last_connected and current_connected:
# Connection restored (might have reconnected automatically)
logger.info("Radio connection restored")
broadcast_health(True, self._port)
broadcast_health(True, self._connection_info)
self._last_connected = True
except asyncio.CancelledError:
+4 -4
View File
@@ -13,12 +13,12 @@ router = APIRouter(tags=["health"])
class HealthResponse(BaseModel):
status: str
radio_connected: bool
serial_port: str | None
connection_info: str | None
database_size_mb: float
oldest_undecrypted_timestamp: int | None
async def build_health_data(radio_connected: bool, serial_port: str | None) -> dict:
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
try:
@@ -36,7 +36,7 @@ async def build_health_data(radio_connected: bool, serial_port: str | None) -> d
return {
"status": "ok" if radio_connected else "degraded",
"radio_connected": radio_connected,
"serial_port": serial_port,
"connection_info": connection_info,
"database_size_mb": db_size_mb,
"oldest_undecrypted_timestamp": oldest_ts,
}
@@ -45,5 +45,5 @@ async def build_health_data(radio_connected: bool, serial_port: str | None) -> d
@router.get("/health", response_model=HealthResponse)
async def healthcheck() -> HealthResponse:
"""Check if the API is running and if the radio is connected."""
data = await build_health_data(radio_manager.is_connected, radio_manager.port)
data = await build_health_data(radio_manager.is_connected, radio_manager.connection_info)
return HealthResponse(**data)
+3 -1
View File
@@ -23,7 +23,9 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
# Send initial health status
try:
health_data = await build_health_data(radio_manager.is_connected, radio_manager.port)
health_data = await build_health_data(
radio_manager.is_connected, radio_manager.connection_info
)
await ws_manager.send_personal(websocket, "health", health_data)
except Exception as e:
+2 -2
View File
@@ -123,13 +123,13 @@ def broadcast_success(message: str, details: str | None = None) -> None:
asyncio.create_task(ws_manager.broadcast("success", data))
def broadcast_health(radio_connected: bool, serial_port: str | None = None) -> None:
def broadcast_health(radio_connected: bool, connection_info: str | None = None) -> None:
"""Broadcast health status change to all connected clients."""
async def _broadcast():
from app.routers.health import build_health_data
data = await build_health_data(radio_connected, serial_port)
data = await build_health_data(radio_connected, connection_info)
await ws_manager.broadcast("health", data)
asyncio.create_task(_broadcast())
@@ -1,4 +1,4 @@
import{r as be,j as vr}from"./index-CWnjp-zX.js";function wr(){return wr=Object.assign?Object.assign.bind():function(n){for(var e=1;e<arguments.length;e++){var t=arguments[e];for(var i in t)({}).hasOwnProperty.call(t,i)&&(n[i]=t[i])}return n},wr.apply(null,arguments)}function Tu(n,e){if(n==null)return{};var t={};for(var i in n)if({}.hasOwnProperty.call(n,i)){if(e.indexOf(i)!==-1)continue;t[i]=n[i]}return t}let Tr=[],dh=[];(()=>{let n="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o".split(",").map(e=>e?parseInt(e,36):1);for(let e=0,t=0;e<n.length;e++)(e%2?dh:Tr).push(t=t+n[e])})();function Cu(n){if(n<768)return!1;for(let e=0,t=Tr.length;;){let i=e+t>>1;if(n<Tr[i])t=i;else if(n>=dh[i])e=i+1;else return!0;if(e==t)return!1}}function hl(n){return n>=127462&&n<=127487}const cl=8205;function Qu(n,e,t=!0,i=!0){return(t?ph:Au)(n,e,i)}function ph(n,e,t){if(e==n.length)return e;e&&mh(n.charCodeAt(e))&&gh(n.charCodeAt(e-1))&&e--;let i=Ls(n,e);for(e+=fl(i);e<n.length;){let s=Ls(n,e);if(i==cl||s==cl||t&&Cu(s))e+=fl(s),i=s;else if(hl(s)){let r=0,o=e-2;for(;o>=0&&hl(Ls(n,o));)r++,o-=2;if(r%2==0)break;e+=2}else break}return e}function Au(n,e,t){for(;e>0;){let i=ph(n,e-2,t);if(i<e)return i;e--}return 0}function Ls(n,e){let t=n.charCodeAt(e);if(!gh(t)||e+1==n.length)return t;let i=n.charCodeAt(e+1);return mh(i)?(t-55296<<10)+(i-56320)+65536:t}function mh(n){return n>=56320&&n<57344}function gh(n){return n>=55296&&n<56320}function fl(n){return n<65536?1:2}class V{lineAt(e){if(e<0||e>this.length)throw new RangeError(`Invalid position ${e} in document of length ${this.length}`);return this.lineInner(e,!1,1,0)}line(e){if(e<1||e>this.lines)throw new RangeError(`Invalid line number ${e} in ${this.lines}-line document`);return this.lineInner(e,!0,1,0)}replace(e,t,i){[e,t]=oi(this,e,t);let s=[];return this.decompose(0,e,s,2),i.length&&i.decompose(0,i.length,s,3),this.decompose(t,this.length,s,1),Ye.from(s,this.length-(t-e)+i.length)}append(e){return this.replace(this.length,this.length,e)}slice(e,t=this.length){[e,t]=oi(this,e,t);let i=[];return this.decompose(e,t,i,0),Ye.from(i,t-e)}eq(e){if(e==this)return!0;if(e.length!=this.length||e.lines!=this.lines)return!1;let t=this.scanIdentical(e,1),i=this.length-this.scanIdentical(e,-1),s=new Qi(this),r=new Qi(e);for(let o=t,l=t;;){if(s.next(o),r.next(o),o=0,s.lineBreak!=r.lineBreak||s.done!=r.done||s.value!=r.value)return!1;if(l+=s.value.length,s.done||l>=i)return!0}}iter(e=1){return new Qi(this,e)}iterRange(e,t=this.length){return new Oh(this,e,t)}iterLines(e,t){let i;if(e==null)i=this.iter();else{t==null&&(t=this.lines+1);let s=this.line(e).from;i=this.iterRange(s,Math.max(s,t==this.lines+1?this.length:t<=1?0:this.line(t-1).to))}return new yh(i)}toString(){return this.sliceString(0)}toJSON(){let e=[];return this.flatten(e),e}constructor(){}static of(e){if(e.length==0)throw new RangeError("A document must have at least one line");return e.length==1&&!e[0]?V.empty:e.length<=32?new Y(e):Ye.from(Y.split(e,[]))}}class Y extends V{constructor(e,t=Pu(e)){super(),this.text=e,this.length=t}get lines(){return this.text.length}get children(){return null}lineInner(e,t,i,s){for(let r=0;;r++){let o=this.text[r],l=s+o.length;if((t?i:l)>=e)return new Mu(s,l,i,o);s=l+1,i++}}decompose(e,t,i,s){let r=e<=0&&t>=this.length?this:new Y(ul(this.text,e,t),Math.min(t,this.length)-Math.max(0,e));if(s&1){let o=i.pop(),l=$n(r.text,o.text.slice(),0,r.length);if(l.length<=32)i.push(new Y(l,o.length+r.length));else{let a=l.length>>1;i.push(new Y(l.slice(0,a)),new Y(l.slice(a)))}}else i.push(r)}replace(e,t,i){if(!(i instanceof Y))return super.replace(e,t,i);[e,t]=oi(this,e,t);let s=$n(this.text,$n(i.text,ul(this.text,0,e)),t),r=this.length+i.length-(t-e);return sLine truncated
import{r as be,j as vr}from"./index-BteQsTFF.js";function wr(){return wr=Object.assign?Object.assign.bind():function(n){for(var e=1;e<arguments.length;e++){var t=arguments[e];for(var i in t)({}).hasOwnProperty.call(t,i)&&(n[i]=t[i])}return n},wr.apply(null,arguments)}function Tu(n,e){if(n==null)return{};var t={};for(var i in n)if({}.hasOwnProperty.call(n,i)){if(e.indexOf(i)!==-1)continue;t[i]=n[i]}return t}let Tr=[],dh=[];(()=>{let n="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o".split(",").map(e=>e?parseInt(e,36):1);for(let e=0,t=0;e<n.length;e++)(e%2?dh:Tr).push(t=t+n[e])})();function Cu(n){if(n<768)return!1;for(let e=0,t=Tr.length;;){let i=e+t>>1;if(n<Tr[i])t=i;else if(n>=dh[i])e=i+1;else return!0;if(e==t)return!1}}function hl(n){return n>=127462&&n<=127487}const cl=8205;function Qu(n,e,t=!0,i=!0){return(t?ph:Au)(n,e,i)}function ph(n,e,t){if(e==n.length)return e;e&&mh(n.charCodeAt(e))&&gh(n.charCodeAt(e-1))&&e--;let i=Ls(n,e);for(e+=fl(i);e<n.length;){let s=Ls(n,e);if(i==cl||s==cl||t&&Cu(s))e+=fl(s),i=s;else if(hl(s)){let r=0,o=e-2;for(;o>=0&&hl(Ls(n,o));)r++,o-=2;if(r%2==0)break;e+=2}else break}return e}function Au(n,e,t){for(;e>0;){let i=ph(n,e-2,t);if(i<e)return i;e--}return 0}function Ls(n,e){let t=n.charCodeAt(e);if(!gh(t)||e+1==n.length)return t;let i=n.charCodeAt(e+1);return mh(i)?(t-55296<<10)+(i-56320)+65536:t}function mh(n){return n>=56320&&n<57344}function gh(n){return n>=55296&&n<56320}function fl(n){return n<65536?1:2}class V{lineAt(e){if(e<0||e>this.length)throw new RangeError(`Invalid position ${e} in document of length ${this.length}`);return this.lineInner(e,!1,1,0)}line(e){if(e<1||e>this.lines)throw new RangeError(`Invalid line number ${e} in ${this.lines}-line document`);return this.lineInner(e,!0,1,0)}replace(e,t,i){[e,t]=oi(this,e,t);let s=[];return this.decompose(0,e,s,2),i.length&&i.decompose(0,i.length,s,3),this.decompose(t,this.length,s,1),Ye.from(s,this.length-(t-e)+i.length)}append(e){return this.replace(this.length,this.length,e)}slice(e,t=this.length){[e,t]=oi(this,e,t);let i=[];return this.decompose(e,t,i,0),Ye.from(i,t-e)}eq(e){if(e==this)return!0;if(e.length!=this.length||e.lines!=this.lines)return!1;let t=this.scanIdentical(e,1),i=this.length-this.scanIdentical(e,-1),s=new Qi(this),r=new Qi(e);for(let o=t,l=t;;){if(s.next(o),r.next(o),o=0,s.lineBreak!=r.lineBreak||s.done!=r.done||s.value!=r.value)return!1;if(l+=s.value.length,s.done||l>=i)return!0}}iter(e=1){return new Qi(this,e)}iterRange(e,t=this.length){return new Oh(this,e,t)}iterLines(e,t){let i;if(e==null)i=this.iter();else{t==null&&(t=this.lines+1);let s=this.line(e).from;i=this.iterRange(s,Math.max(s,t==this.lines+1?this.length:t<=1?0:this.line(t-1).to))}return new yh(i)}toString(){return this.sliceString(0)}toJSON(){let e=[];return this.flatten(e),e}constructor(){}static of(e){if(e.length==0)throw new RangeError("A document must have at least one line");return e.length==1&&!e[0]?V.empty:e.length<=32?new Y(e):Ye.from(Y.split(e,[]))}}class Y extends V{constructor(e,t=Pu(e)){super(),this.text=e,this.length=t}get lines(){return this.text.length}get children(){return null}lineInner(e,t,i,s){for(let r=0;;r++){let o=this.text[r],l=s+o.length;if((t?i:l)>=e)return new Mu(s,l,i,o);s=l+1,i++}}decompose(e,t,i,s){let r=e<=0&&t>=this.length?this:new Y(ul(this.text,e,t),Math.min(t,this.length)-Math.max(0,e));if(s&1){let o=i.pop(),l=$n(r.text,o.text.slice(),0,r.length);if(l.length<=32)i.push(new Y(l,o.length+r.length));else{let a=l.length>>1;i.push(new Y(l.slice(0,a)),new Y(l.slice(a)))}}else i.push(r)}replace(e,t,i){if(!(i instanceof Y))return super.replace(e,t,i);[e,t]=oi(this,e,t);let s=$n(this.text,$n(i.text,ul(this.text,0,e)),t),r=this.length+i.length-(t-e);return sLine truncated
`){[e,t]=oi(this,e,t);let s="";for(let r=0,o=0;r<=t&&o<this.text.length;o++){let l=this.text[o],a=r+l.length;r>e&&o&&(s+=i),e<a&&t>r&&(s+=l.slice(Math.max(0,e-r),t-r)),r=a+1}return s}flatten(e){for(let t of this.text)e.push(t)}scanIdentical(){return 0}static split(e,t){let i=[],s=-1;for(let r of e)i.push(r),s+=r.length+1,i.length==32&&(t.push(new Y(i,s)),i=[],s=-1);return s>-1&&t.push(new Y(i,s)),t}}class Ye extends V{constructor(e,t){super(),this.children=e,this.length=t,this.lines=0;for(let i of e)this.lines+=i.lines}lineInner(e,t,i,s){for(let r=0;;r++){let o=this.children[r],l=s+o.length,a=i+o.lines-1;if((t?a:l)>=e)return o.lineInner(e,t,i,s);s=l+1,i=a+1}}decompose(e,t,i,s){for(let r=0,o=0;o<=t&&r<this.children.length;r++){let l=this.children[r],a=o+l.length;if(e<=a&&t>=o){let h=s&((o<=e?1:0)|(a>=t?2:0));o>=e&&a<=t&&!h?i.push(l):l.decompose(e-o,t-o,i,h)}o=a+1}}replace(e,t,i){if([e,t]=oi(this,e,t),i.lines<this.lines)for(let s=0,r=0;s<this.children.length;s++){let o=this.children[s],l=r+o.length;if(e>=r&&t<=l){let a=o.replace(e-r,t-r,i),h=this.lines-o.lines+a.lines;if(a.lines<h>>4&&a.lines>h>>6){let c=this.children.slice();return c[s]=a,new Ye(c,this.length-(t-e)+i.length)}return super.replace(r,l,a)}r=l+1}return super.replace(e,t,i)}sliceString(e,t=this.length,i=`
`){[e,t]=oi(this,e,t);let s="";for(let r=0,o=0;r<this.children.length&&o<=t;r++){let l=this.children[r],a=o+l.length;o>e&&r&&(s+=i),e<a&&t>o&&(s+=l.sliceString(e-o,t-o,i)),o=a+1}return s}flatten(e){for(let t of this.children)t.flatten(e)}scanIdentical(e,t){if(!(e instanceof Ye))return 0;let i=0,[s,r,o,l]=t>0?[0,0,this.children.length,e.children.length]:[this.children.length-1,e.children.length-1,-1,-1];for(;;s+=t,r+=t){if(s==o||r==l)return i;let a=this.children[s],h=e.children[r];if(a!=h)return i+a.scanIdentical(h,t);i+=a.length+1}}static from(e,t=e.reduce((i,s)=>i+s.length+1,-1)){let i=0;for(let d of e)i+=d.lines;if(i<32){let d=[];for(let p of e)p.flatten(d);return new Y(d,t)}let s=Math.max(32,i>>5),r=s<<1,o=s>>1,l=[],a=0,h=-1,c=[];function f(d){let p;if(d.lines>r&&d instanceof Ye)for(let m of d.children)f(m);else d.lines>o&&(a>o||!a)?(u(),l.push(d)):d instanceof Y&&a&&(p=c[c.length-1])instanceof Y&&d.lines+p.lines<=32?(a+=d.lines,h+=d.length+1,c[c.length-1]=new Y(p.text.concat(d.text),p.length+1+d.length)):(a+d.lines>s&&u(),a+=d.lines,h+=d.length+1,c.push(d))}function u(){a!=0&&(l.push(c.length==1?c[0]:Ye.from(c,h)),h=-1,a=c.length=0)}for(let d of e)f(d);return u(),l.length==1?l[0]:new Ye(l,t)}}V.empty=new Y([""],0);function Pu(n){let e=-1;for(let t of n)e+=t.length+1;return e}function $n(n,e,t=0,i=1e9){for(let s=0,r=0,o=!0;r<n.length&&s<=i;r++){let l=n[r],a=s+l.length;a>=t&&(a>i&&(l=l.slice(0,i-s)),s<t&&(l=l.slice(t-s)),o?(e[e.length-1]+=l,o=!1):e.push(l)),s=a+1}return e}function ul(n,e,t){return $n(n,[""],e,t)}class Qi{constructor(e,t=1){this.dir=t,this.done=!1,this.lineBreak=!1,this.value="",this.nodes=[e],this.offsets=[t>0?1:(e instanceof Y?e.text.length:e.children.length)<<1]}nextInner(e,t){for(this.done=this.lineBreak=!1;;){let i=this.nodes.length-1,s=this.nodes[i],r=this.offsets[i],o=r>>1,l=s instanceof Y?s.text.length:s.children.length;if(o==(t>0?l:0)){if(i==0)return this.done=!0,this.value="",this;t>0&&this.offsets[i-1]++,this.nodes.pop(),this.offsets.pop()}else if((r&1)==(t>0?0:1)){if(this.offsets[i]+=t,e==0)return this.lineBreak=!0,this.value=`
`,this;e--}else if(s instanceof Y){let a=s.text[o+(t<0?-1:0)];if(this.offsets[i]+=t,a.length>Math.max(0,e))return this.value=e==0?a:t>0?a.slice(e):a.slice(0,a.length-e),this;e-=a.length}else{let a=s.children[o+(t<0?-1:0)];e>a.length?(e-=a.length,this.offsets[i]+=t):(t<0&&this.offsets[i]--,this.nodes.push(a),this.offsets.push(t>0?1:(a instanceof Y?a.text.length:a.children.length)<<1))}}}next(e=0){return e<0&&(this.nextInner(-e,-this.dir),e=this.value.length),this.nextInner(e,this.dir)}}class Oh{constructor(e,t,i){this.value="",this.done=!1,this.cursor=new Qi(e,t>i?-1:1),this.pos=t>i?e.length:0,this.from=Math.min(t,i),this.to=Math.max(t,i)}nextInner(e,t){if(t<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;e+=Math.max(0,t<0?this.pos-this.to:this.from-this.pos);let i=t<0?this.pos-this.from:this.to-this.pos;e>i&&(e=i),i-=e;let{value:s}=this.cursor.next(e);return this.pos+=(s.length+e)*t,this.value=s.length<=i?s:t<0?s.slice(s.length-i):s.slice(0,i),this.done=!this.value,this}next(e=0){return e<0?e=Math.max(e,this.from-this.pos):e>0&&(e=Math.min(e,this.to-this.pos)),this.nextInner(e,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&this.value!=""}}class yh{constructor(e){this.inner=e,this.afterBreak=!0,this.value="",this.done=!1}next(e=0){let{done:t,lineBreak:i,value:s}=this.inner.next(e);return t&&this.afterBreak?(this.value="",this.afterBreak=!1):t?(this.done=!0,this.value=""):i?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=s,this.afterBreak=!1),this}get lineBreak(){return!1}}typeof Symbol<"u"&&(V.prototype[Symbol.iterator]=function(){return this.iter()},Qi.prototype[Symbol.iterator]=Oh.prototype[Symbol.iterator]=yh.prototype[Symbol.iterator]=function(){return this});class Mu{constructor(e,t,i,s){this.from=e,this.to=t,this.number=i,this.text=s}get length(){return this.to-this.from}}function oi(n,e,t){return e=Math.max(0,Math.min(n.length,e)),[e,Math.max(e,Math.min(n.length,t))]}function re(n,e,t=!0,i=!0){return Qu(n,e,t,i)}function Ru(n){return n>=56320&&n<57344}function Eu(n){return n>=55296&&n<56320}function ke(n,e){let t=n.charCodeAt(e);if(!Eu(t)||e+1==n.length)return t;let i=n.charCodeAt(e+1);return Ru(i)?(t-55296<<10)+(i-56320)+65536:t}function xo(n){return n<=65535?String.fromCharCode(n):(n-=65536,String.fromCharCode((n>>10)+55296,(n&1023)+56320))}function Ke(n){return n<65536?1:2}const Cr=/\r\n?|\n/;var de=(function(n){return n[n.Simple=0]="Simple",n[n.TrackDel=1]="TrackDel",n[n.TrackBefore=2]="TrackBefore",n[n.TrackAfter=3]="TrackAfter",n})(de||(de={}));class nt{constructor(e){this.sections=e}get length(){let e=0;for(let t=0;t<this.sections.length;t+=2)e+=this.sections[t];return e}get newLength(){let e=0;for(let t=0;t<this.sections.length;t+=2){let i=this.sections[t+1];e+=i<0?this.sections[t]:i}return e}get empty(){return this.sections.length==0||this.sections.length==2&&this.sections[1]<0}iterGaps(e){for(let t=0,i=0,s=0;t<this.sections.length;){let r=this.sections[t++],o=this.sections[t++];o<0?(e(i,s,r),s+=r):s+=o,i+=r}}iterChangedRanges(e,t=!1){Qr(this,e,t)}get invertedDesc(){let e=[];for(let t=0;t<this.sections.length;){let i=this.sections[t++],s=this.sections[t++];s<0?e.push(i,s):e.push(s,i)}return new nt(e)}composeDesc(e){return this.empty?e:e.empty?this:bh(this,e)}mapDesc(e,t=!1){return e.empty?this:Ar(this,e,t)}mapPos(e,t=-1,i=de.Simple){let s=0,r=0;for(let o=0;o<this.sections.length;){let l=this.sections[o++],a=this.sections[o++],h=s+l;if(a<0){if(h>e)return r+(e-s);r+=l}else{if(i!=de.Simple&&h>=e&&(i==de.TrackDel&&s<e&&h>e||i==de.TrackBefore&&s<e||i==de.TrackAfter&&h>e))return null;if(h>e||h==e&&t<0&&!l)return e==s||t<0?r:r+a;r+=a}s=h}if(e>s)throw new RangeError(`Position ${e} is out of range for changeset of length ${s}`);return r}touchesRange(e,t=e){for(let i=0,s=0;i<this.sections.length&&s<=t;){let r=this.sections[i++],o=this.sections[i++],l=s+r;if(o>=0&&s<=t&&l>=e)return s<e&&l>t?"cover":!0;s=l}return!1}toString(){let e="";for(let t=0;t<this.sections.length;){let i=this.sections[t++],s=this.sections[t++];e+=(e?" ":"")+i+(s>=0?":"+s:"")}return e}toJSON(){return this.sections}static fromJSON(e){if(!Array.isArray(e)||e.length%2||e.some(t=>typeof t!="number"))throw new RangeError("Invalid JSON representation of ChangeDesc");return new nt(e)}static create(e){return new nt(e)}}class se extends nt{constructor(e,t){super(e),this.inserted=t}apply(e){if(this.length!=e.length)throw new RangeError("Applying change set to a document with the wrong length");return Qr(this,(t,i,s,r,o)=>e=e.replace(s,s+(i-t),o),!1),e}mapDesc(e,t=!1){return Ar(this,e,t,!0)}invert(e){let t=this.sections.slice(),i=[];for(let s=0,r=0;s<t.length;s+=2){let o=t[s],l=t[s+1];if(l>=0){t[s]=l,t[s+1]=o;let a=s>>1;for(;i.length<a;)i.push(V.empty);i.push(o?e.slice(r,r+o):V.empty)}r+=o}return new se(t,i)}compose(e){return this.empty?e:e.empty?this:bh(this,e,!0)}map(e,t=!1){return e.empty?this:Ar(this,e,t,!0)}iterChanges(e,t=!1){Qr(this,e,t)}get desc(){return nt.crea Line truncated
@@ -12,4 +12,4 @@ import{r as be,j as vr}from"./index-CWnjp-zX.js";function wr(){return wr=Object.
`:i=="r"?"\r":i=="t"?" ":"\\")}eq(e){return this.search==e.search&&this.replace==e.replace&&this.caseSensitive==e.caseSensitive&&this.regexp==e.regexp&&this.wholeWord==e.wholeWord&&this.test==e.test}create(){return this.regexp?new S0(this):new y0(this)}getCursor(e,t=0,i){let s=e.doc?e:N.create({doc:e});return i==null&&(i=s.doc.length),this.regexp?jt(this,s,t,i):Ht(this,s,t,i)}}class Uf{constructor(e){this.spec=e}}function g0(n,e,t){return(i,s,r,o)=>{if(t&&!t(i,s,r,o))return!1;let l=i>=o&&s<=o+r.length?r.slice(i-o,s-o):e.doc.sliceString(i,s);return n(l,e,i,s)}}function Ht(n,e,t,i){let s;return n.wholeWord&&(s=O0(e.doc,e.charCategorizer(e.selection.main.head))),n.test&&(s=g0(n.test,e,s)),new ui(e.doc,n.unquoted,t,i,n.caseSensitive?void 0:r=>r.toLowerCase(),s)}function O0(n,e){return(t,i,s,r)=>((r>t||r+s.length<i)&&(r=Math.max(0,t-2),s=n.sliceString(r,Math.min(n.length,i+2))),(e(ds(s,t-r))!=G.Word||e(ps(s,t-r))!=G.Word)&&(e(ps(s,i-r))!=G.Word||e(ds(s,i-r))!=G.Word))}class y0 extends Uf{constructor(e){super(e)}nextMatch(e,t,i){let s=Ht(this.spec,e,i,e.doc.length).nextOverlapping();if(s.done){let r=Math.min(e.doc.length,t+this.spec.unquoted.length);s=Ht(this.spec,e,0,r).nextOverlapping()}return s.done||s.value.from==t&&s.value.to==i?null:s.value}prevMatchInRange(e,t,i){for(let s=i;;){let r=Math.max(t,s-1e4-this.spec.unquoted.length),o=Ht(this.spec,e,r,s),l=null;for(;!o.nextOverlapping().done;)l=o.value;if(l)return l;if(r==t)return null;s-=1e4}}prevMatch(e,t,i){let s=this.prevMatchInRange(e,0,t);return s||(s=this.prevMatchInRange(e,Math.max(0,i-this.spec.unquoted.length),e.doc.length)),s&&(s.from!=t||s.to!=i)?s:null}getReplacement(e){return this.spec.unquote(this.spec.replace)}matchAll(e,t){let i=Ht(this.spec,e,0,e.doc.length),s=[];for(;!i.next().done;){if(s.length>=t)return null;s.push(i.value)}return s}highlight(e,t,i,s){let r=Ht(this.spec,e,Math.max(0,t-this.spec.unquoted.length),Math.min(i+this.spec.unquoted.length,e.doc.length));for(;!r.next().done;)s(r.value.from,r.value.to)}}function b0(n,e,t){return(i,s,r)=>(!t||t(i,s,r))&&n(r[0],e,i,s)}function jt(n,e,t,i){let s;return n.wholeWord&&(s=x0(e.charCategorizer(e.selection.main.head))),n.test&&(s=b0(n.test,e,s)),new Xf(e.doc,n.search,{ignoreCase:!n.caseSensitive,test:s},t,i)}function ds(n,e){return n.slice(re(n,e,!1),e)}function ps(n,e){return n.slice(e,re(n,e))}function x0(n){return(e,t,i)=>!i[0].length||(n(ds(i.input,i.index))!=G.Word||n(ps(i.input,i.index))!=G.Word)&&(n(ps(i.input,i.index+i[0].length))!=G.Word||n(ds(i.input,i.index+i[0].length))!=G.Word)}class S0 extends Uf{nextMatch(e,t,i){let s=jt(this.spec,e,i,e.doc.length).next();return s.done&&(s=jt(this.spec,e,0,t).next()),s.done?null:s.value}prevMatchInRange(e,t,i){for(let s=1;;s++){let r=Math.max(t,i-s*1e4),o=jt(this.spec,e,r,i),l=null;for(;!o.next().done;)l=o.value;if(l&&(r==t||l.from>r+10))return l;if(r==t)return null}}prevMatch(e,t,i){return this.prevMatchInRange(e,0,t)||this.prevMatchInRange(e,i,e.doc.length)}getReplacement(e){return this.spec.unquote(this.spec.replace).replace(/\$([$&]|\d+)/g,(t,i)=>{if(i=="&")return e.match[0];if(i=="$")return"$";for(let s=i.length;s>0;s--){let r=+i.slice(0,s);if(r>0&&r<e.match.length)return e.match[r]+i.slice(s)}return t})}matchAll(e,t){let i=jt(this.spec,e,0,e.doc.length),s=[];for(;!i.next().done;){if(s.length>=t)return null;s.push(i.value)}return s}highlight(e,t,i,s){let r=jt(this.spec,e,Math.max(0,t-250),Math.min(i+250,e.doc.length));for(;!r.next().done;)s(r.value.from,r.value.to)}}const Ui=D.define(),Go=D.define(),kt=he.define({create(n){return new cr(Oo(n).create(),null)},update(n,e){for(let t of e.effects)t.is(Ui)?n=new cr(t.value.create(),n.panel):t.is(Go)&&(n=new cr(n.query,t.value?Zo:null));return n},provide:n=>Vi.from(n,e=>e.panel)});class cr{constructor(e,t){this.query=e,this.panel=t}}const k0=M.mark({class:"cm-searchMatch"}),v0=M.mark({class:"cm-searchMatch cm-searchMatch-selected"}),w0=J.fromClass(class{constructor(n){this.view=n,this.decorations=this.highlight(n.state.field(kt))}update(n){let e=n.state.field(kt);(e!=n.startState.field(kt)||n.docChanged||n.selectionSet||n.viewportChanged)&&(this.decorations=this.highlight(e))}highlight({query:n,panel:e}){if(!e||!n.spec.valid)return M.none;let{view:t}=this,i=new pt;for(let s=0,r=t.visibleRanges,o=r.length;s<o;s++){let{from:l,to:a}=r[s];for(;s<o-1&&a>r[s+1].from-500;)a=r[++s].to;n.highlight(t.state,l,a,(h,c)=>{let f=t.state.selection.ranges.some(u=>u.from==h&&u.to==c);i.add(h,c,f?v0:k0)})}return i.finish()}},{decorations:n=>n.decorations});function hn(n){return e=>{let t=e.state.field(kt,!1);return t&&t.query.spec.valid?n(e,t):Gf(e)}}const ms=hn((n,{query:e})=>{let{to:t}=n.state.selection.main,i=e.nextMatch(n.state,t,t);if(!i)return!1;let s=b.single(i.from,i.to),r=n.state.facet(gi);return n.dispatch({selection:s,effects:[Yo(n,i),r.scrollToMatch(s.main,n)],userEvent:"select.search"}),jf(n),!0}),gs=hn((n,{query:e})=>{let{state:t}=n,{from:i}=t.selection.main,s=e.prevMatch(t,i,i);if(!s)return!1;let r= Line truncated
`,{label:"if",detail:"block",type:"keyword"}),ot("if ${}:\n ${}\nelse:\n ${}",{label:"if",detail:"/ else block",type:"keyword"}),ot("class ${name}:\n def __init__(self, ${params}):\n ${}",{label:"class",detail:"definition",type:"keyword"}),ot("import ${module}",{label:"import",detail:"statement",type:"keyword"}),ot("from ${module} import ${names}",{label:"from",detail:"import",type:"keyword"})],Rb=D0(wu,Kf(Pb.concat(Mb)));function xr(n){let{node:e,pos:t}=n,i=n.lineIndent(t,-1),s=null;for(;;){let r=e.childBefore(t);if(r)if(r.name=="Comment")t=r.from;else if(r.name=="Body"||r.name=="MatchBody")n.baseIndentFor(r)+n.unit<=i&&(s=r),e=r;else if(r.name=="MatchClause")e=r;else if(r.type.is("Statement"))e=r;else break;else break}return s}function Sr(n,e){let t=n.baseIndentFor(e),i=n.lineAt(n.pos,-1),s=i.from+i.text.length;return/^\s*($|#)/.test(i.text)&&n.node.to<s+100&&!/\S/.test(n.state.sliceDoc(s,n.node.to))&&n.lineIndent(n.pos,-1)<=t||/^\s*(else:|elif |except |finally:|case\s+[^=:]+:)/.test(n.textAfter)&&n.lineIndent(n.pos,-1)>t?null:t+n.unit}const kr=os.define({name:"python",parser:Cb.configure({props:[Uc.add({Body:n=>{var e;let t=/^\s*(#|$)/.test(n.textAfter)&&xr(n)||n.node;return(e=Sr(n,t))!==null&&e!==void 0?e:n.continue()},MatchBody:n=>{var e;let t=xr(n);return(e=Sr(n,t||n.node))!==null&&e!==void 0?e:n.continue()},IfStatement:n=>/^\s*(else:|elif )/.test(n.textAfter)?n.baseIndent:n.continue(),"ForStatement WhileStatement":n=>/^\s*else:/.test(n.textAfter)?n.baseIndent:n.continue(),TryStatement:n=>/^\s*(except[ :]|finally:|else:)/.test(n.textAfter)?n.baseIndent:n.continue(),MatchStatement:n=>/^\s*case /.test(n.textAfter)?n.baseIndent+n.unit:n.continue(),"TupleExpression ComprehensionExpression ParamList ArgList ParenthesizedExpression":sr({closing:")"}),"DictionaryExpression DictionaryComprehensionExpression SetExpression SetComprehensionExpression":sr({closing:"}"}),"ArrayExpression ArrayComprehensionExpression":sr({closing:"]"}),MemberExpression:n=>n.baseIndent+n.unit,"String FormatString":()=>null,Script:n=>{var e;let t=xr(n);return(e=t&&Sr(n,t))!==null&&e!==void 0?e:n.continue()}}),Gc.add({"ArrayExpression DictionaryExpression SetExpression TupleExpression":pg,Body:(n,e)=>({from:n.from+1,to:n.to-(n.to==e.doc.length?0:1)}),"String FormatString":(n,e)=>({from:e.doc.lineAt(n.from).to,to:n.to})})]}),languageData:{closeBrackets:{brackets:["(","[","{","'",'"',"'''",'"""'],stringPrefixes:["f","fr","rf","r","u","b","br","rb","F","FR","RF","R","U","B","BR","RB"]},commentTokens:{line:"#"},indentOnInput:/^\s*([\}\]\)]|else:|elif |except |finally:|case\s+[^:]*:?)$/}});function Eb(){return new ng(kr,[kr.data.of({autocomplete:Ab}),kr.data.of({autocomplete:Rb})])}function Bb({value:n,onChange:e,id:t}){return vr.jsx("div",{className:"w-full overflow-hidden rounded-md border border-input",children:vr.jsx(pu,{value:n,onChange:e,extensions:[Eb()],theme:du,height:"256px",basicSetup:{lineNumbers:!0,foldGutter:!1,highlightActiveLine:!0},className:"text-sm",id:t})})}export{Bb as BotCodeEditor};
//# sourceMappingURL=BotCodeEditor-B21OpXam.js.map
//# sourceMappingURL=BotCodeEditor-BNshQLhC.js.map
@@ -1 +1 @@
{"version":3,"file":"BotCodeEditor-B21OpXam.js","sources":["../../node_modules/@babel/runtime/helpers/esm/extends.js","../../node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js","../../node_modules/@marijn/find-cluster-break/src/index.js","../../node_modules/@codemirror/state/dist/index.js","../../node_modules/style-mod/src/style-mod.js","../../node_modules/w3c-keyname/index.js","../../node_modules/crelt/index.js","../../node_modules/@codemirror/view/dist/index.js","../../node_modules/@lezer/common/dist/index.js","../../node_modules/@lezer/highlight/dist/index.js","../../node_modules/@codemirror/language/dist/index.js","../../node_modules/@codemirror/commands/dist/index.js","../../node_modules/@codemirror/search/dist/index.js","../../node_modules/@codemirror/autocomplete/dist/index.js","../../node_modules/@codemirror/lint/dist/index.js","../../node_modules/@uiw/codemirror-extensions-basic-setup/esm/index.js","../../node_modules/@codemirror/theme-one-dark/dist/index.js","../../node_modules/@uiw/react-codemirror/esm/theme/light.js","../../node_modules/@uiw/react-codemirror/esm/getDefaultExtensions.js","../../node_modules/@uiw/react-codemirror/esm/utils.js","../../node_modules/@uiw/react-codemirror/esm/timeoutLatch.js","../../node_modules/@uiw/react-codemirror/esm/useCodeMirror.js","../../node_modules/@uiw/react-codemirror/esm/index.js","../../node_modules/@lezer/lr/dist/index.js","../../node_modules/@lezer/python/dist/index.js","../../node_modules/@codemirror/lang-python/dist/index.js","../../src/components/BotCodeEditor.tsx"],"sourcesContent":["function _extends() {\n return _extends = Object.assign ? Object.assign.bind() : function (n) {\n for (var e = 1; e < arguments.length; e++) {\n var t = arguments[e];\n for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);\n }\n return n;\n }, _extends.apply(null, arguments);\n}\nexport { _extends as default };","function _objectWithoutPropertiesLoose(r, e) {\n if (null == r) return {};\n var t = {};\n for (var n in r) if ({}.hasOwnProperty.call(r, n)) {\n if (-1 !== e.indexOf(n)) continue;\n t[n] = r[n];\n }\n return t;\n}\nexport { _objectWithoutPropertiesLoose as default };","// These are filled with ranges (rangeFrom[i] up to but not including\n// rangeTo[i]) of code points that count as extending characters.\nlet rangeFrom = [], rangeTo = []\n\n;(() => {\n // Compressed representation of the Grapheme_Cluster_Break=Extend\n // information from\n // http://www.unicode.org/Public/16.0.0/ucd/auxiliary/GraphemeBreakProperty.txt.\n // Each pair of elements represents a range, as an offet from the\n // previous range and a length. Numbers are in base-36, with the empty\n // string being a shorthand for 1.\n let numbers = \"lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o\".split(\",\").map(s => s ? parseInt(s, 36) : 1)\n for (let i = 0, n = 0; i < numbers.length; i++)\n (i % 2 ? rangeTo : rangeFrom).push(n = n + numbers[i])\n})()\n\nexport function isExtendingChar(code) {\n if (code < 768) return false\n for (let from = 0, to = rangeFrom.length;;) {\n let mid = (from + to) >> 1\n if (code < rangeFrom[mid]) to = mid\n else if (code >= rangeTo[mid]) from = mid + 1\n else return true\n if (from == to) return false\n }\n}\n\nfunction isRegionalIndicator(code) {\n return code >= 0x1F1E6 && code <= 0x1F1FF\n}\n\nfunction check(code) {\n for (let i = 0; i < rangeFrom.length; i++) {\n if (rangeTo[i] > code) return rangeFrom[i] <= code\n }\n return false\n}\n\nconst ZWJ = 0x200d\n\nexport function findClusterBreak(str, pos, forward = true, includeExtending = true) {\n return (forward ? nextClusterBreak : prevClusterBreak)(strLine truncated
{"version":3,"file":"BotCodeEditor-BNshQLhC.js","sources":["../../node_modules/@babel/runtime/helpers/esm/extends.js","../../node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js","../../node_modules/@marijn/find-cluster-break/src/index.js","../../node_modules/@codemirror/state/dist/index.js","../../node_modules/style-mod/src/style-mod.js","../../node_modules/w3c-keyname/index.js","../../node_modules/crelt/index.js","../../node_modules/@codemirror/view/dist/index.js","../../node_modules/@lezer/common/dist/index.js","../../node_modules/@lezer/highlight/dist/index.js","../../node_modules/@codemirror/language/dist/index.js","../../node_modules/@codemirror/commands/dist/index.js","../../node_modules/@codemirror/search/dist/index.js","../../node_modules/@codemirror/autocomplete/dist/index.js","../../node_modules/@codemirror/lint/dist/index.js","../../node_modules/@uiw/codemirror-extensions-basic-setup/esm/index.js","../../node_modules/@codemirror/theme-one-dark/dist/index.js","../../node_modules/@uiw/react-codemirror/esm/theme/light.js","../../node_modules/@uiw/react-codemirror/esm/getDefaultExtensions.js","../../node_modules/@uiw/react-codemirror/esm/utils.js","../../node_modules/@uiw/react-codemirror/esm/timeoutLatch.js","../../node_modules/@uiw/react-codemirror/esm/useCodeMirror.js","../../node_modules/@uiw/react-codemirror/esm/index.js","../../node_modules/@lezer/lr/dist/index.js","../../node_modules/@lezer/python/dist/index.js","../../node_modules/@codemirror/lang-python/dist/index.js","../../src/components/BotCodeEditor.tsx"],"sourcesContent":["function _extends() {\n return _extends = Object.assign ? Object.assign.bind() : function (n) {\n for (var e = 1; e < arguments.length; e++) {\n var t = arguments[e];\n for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);\n }\n return n;\n }, _extends.apply(null, arguments);\n}\nexport { _extends as default };","function _objectWithoutPropertiesLoose(r, e) {\n if (null == r) return {};\n var t = {};\n for (var n in r) if ({}.hasOwnProperty.call(r, n)) {\n if (-1 !== e.indexOf(n)) continue;\n t[n] = r[n];\n }\n return t;\n}\nexport { _objectWithoutPropertiesLoose as default };","// These are filled with ranges (rangeFrom[i] up to but not including\n// rangeTo[i]) of code points that count as extending characters.\nlet rangeFrom = [], rangeTo = []\n\n;(() => {\n // Compressed representation of the Grapheme_Cluster_Break=Extend\n // information from\n // http://www.unicode.org/Public/16.0.0/ucd/auxiliary/GraphemeBreakProperty.txt.\n // Each pair of elements represents a range, as an offet from the\n // previous range and a length. Numbers are in base-36, with the empty\n // string being a shorthand for 1.\n let numbers = \"lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o\".split(\",\").map(s => s ? parseInt(s, 36) : 1)\n for (let i = 0, n = 0; i < numbers.length; i++)\n (i % 2 ? rangeTo : rangeFrom).push(n = n + numbers[i])\n})()\n\nexport function isExtendingChar(code) {\n if (code < 768) return false\n for (let from = 0, to = rangeFrom.length;;) {\n let mid = (from + to) >> 1\n if (code < rangeFrom[mid]) to = mid\n else if (code >= rangeTo[mid]) from = mid + 1\n else return true\n if (from == to) return false\n }\n}\n\nfunction isRegionalIndicator(code) {\n return code >= 0x1F1E6 && code <= 0x1F1FF\n}\n\nfunction check(code) {\n for (let i = 0; i < rangeFrom.length; i++) {\n if (rangeTo[i] > code) return rangeFrom[i] <= code\n }\n return false\n}\n\nconst ZWJ = 0x200d\n\nexport function findClusterBreak(str, pos, forward = true, includeExtending = true) {\n return (forward ? nextClusterBreak : prevClusterBreak)(strLine truncated
@@ -123,7 +123,7 @@ No ACL entries`;const s=[`ACL (${n.length})`];for(const i of n){const l=i.name||
If you want to hide the \`${s.titleName}\`, you can wrap it with our VisuallyHidden component.
For more information, see https://radix-ui.com/primitives/docs/components/${s.docsSlug}`;return D.useEffect(()=>{n&&(document.getElementById(n)||console.error(i))},[i,n]),null},Cb="DialogDescriptionWarning",Eb=({contentRef:n,descriptionId:s})=>{const l=`Warning: Missing \`Description\` or \`aria-describedby={undefined}\` for {${pA(Cb).contentName}}.`;return D.useEffect(()=>{var u;const c=(u=n.current)==null?void 0:u.getAttribute("aria-describedby");s&&c&&(document.getElementById(s)||console.warn(l))},[l,n,s]),null},mA=tA,gA=iA,sd=oA,ad=sA,ld=lA,cd=uA,xA=fA;const Z0=mA,kb=gA,AA=D.forwardRef(({className:n,...s},i)=>_.jsx(sd,{ref:i,className:dt("fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",n),...s}));AA.displayName=sd.displayName;const ud=D.forwardRef(({className:n,children:s,...i},l)=>_.jsxs(kb,{children:[_.jsx(AA,{}),_.jsxs(ad,{ref:l,className:dt("fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",n),...i,children:[s,_.jsxs(xA,{className:"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",children:[_.jsx(wx,{className:"h-4 w-4"}),_.jsx("span",{className:"sr-only",children:"Close"})]})]})]}));ud.displayName=ad.displayName;const dd=({className:n,...s})=>_.jsx("div",{className:dt("flex flex-col space-y-1.5 text-center sm:text-left",n),...s});dd.displayName="DialogHeader";const W0=({className:n,...s})=>_.jsx("div",{className:dt("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",n),...s});W0.displayName="DialogFooter";const fd=D.forwardRef(({className:n,...s},i)=>_.jsx(ld,{ref:i,className:dt("text-lg font-semibold leading-none tracking-tight",n),...s}));fd.displayName=ld.displayName;const hd=D.forwardRef(({className:n,...s},i)=>_.jsx(cd,{ref:i,className:dt("text-sm text-muted-foreground",n),...s}));hd.displayName=cd.displayName;function Tm(){const n=window.location.hash.slice(1);if(!n)return null;if(n==="raw")return{type:"raw",name:"raw"};if(n==="map")return{type:"map",name:"map"};if(n==="visualizer")return{type:"visualizer",name:"visualizer"};if(n.startsWith("map/focus/")){const c=n.slice(10);return c?{type:"map",name:"map",mapFocusKey:decodeURIComponent(c)}:{type:"map",name:"map"}}const s=n.indexOf("/");if(s===-1)return null;const i=n.slice(0,s),l=decodeURIComponent(n.slice(s+1));return(i==="channel"||i==="contact")&&l?{type:i,name:l}:null}function vA(n){return`#map/focus/${encodeURIComponent(n)}`}function Sb(n){if(!n)return"";if(n.type==="raw")return"#raw";if(n.type==="map")return"#map";if(n.type==="visualizer")return"#visualizer";const s=n.type==="channel"&&n.name.startsWith("#")?n.name.slice(1):n.name;return`#${n.type}/${encodeURIComponent(s)}`}function Bb(n){const s=Sb(n);s!==window.location.hash&&window.history.replaceState(null,"",s||window.location.pathname)}function Pb({open:n,onClose:s,paths:i,senderInfo:l,contacts:c,config:u}){const p=i.map(g=>({...g,resolved:H1(g.path,l,c,u)})),d=i.length===1;return _.jsx(Z0,{open:n,onOpenChange:g=>!g&&s(),children:_.jsxs(ud,{className:"max-w-md max-h-[80vh] flex flex-col",children:[_.jsxs(dd,{children:[_.jsxs(fd,{children:["Message Path",!d&&`s (${i.length})`]}),_.jsx(hd,{children:d?_.jsxs(_.Fragment,{children:["This shows ",_.jsx("em",{children:"one route"})," that this message traveled through the mesh network. Routers may be incorrectly identified due to prefix collisions between heard and non-heard router advertisements."]}):_.jsxs(_.Fragment,{children:["This message was received via ",_.jsxs("strong",{children:[i.length," different routes"]}),". Routers may be incorrectly identified due to prefix collisions."]})})]}),_.jsxs("div",{className:"flex-1 overflow-y-auto py-2 space-y-4",children:[p.map((g,m)=>_.jsxs("div",{children:[!d&&_.jsxs("div",{className:"text-xs text-muted-foreground font-medium mb-2 pb-1 border-b border-border",children:["Path ",m+1," — received ",ra(g.received_at)]}),_.jsx(Tb,{resolved:g.resolved,senderInfo:l,hideStraightLine:!d})]},m)),!d&&p.length>0&&(()=>{const g=p[0].resolved;return Bt(g.sender.lat,g.sender.lon)&&Bt(g.receiver.lat,g.receiver.lon)?_.jsxs("div",{className:"pt-3 mt-1 border-t border-border",children:[_.jsx("span",{className:"text-sm text-muted-foreground",children:"Straight-line distance: "}),_.jsx("span",{className:"text-sm font-medium",c Line truncated
`).map(($,ue,pe)=>_.jsxs("span",{children:[Nb($,d),ue<pe.length-1&&_.jsx("br",{})]},ue)),!re&&_.jsxs(_.Fragment,{children:[_.jsx("span",{className:"text-[10px] text-muted-foreground/50 ml-2",children:ra(j.sender_timestamp||j.received_at)}),!j.outgoing&&j.paths&&j.paths.length>0&&_.jsx(Fm,{paths:j.paths,variant:"inline",onClick:()=>C({paths:j.paths,senderInfo:M(j,Z,ne)})})]}),j.outgoing&&(j.acked>0?` ✓${j.acked>1?j.acked:""}`:" ?")]})]})]},j.id)})]}),b&&_.jsx("button",{onClick:P,className:"absolute bottom-4 right-4 w-10 h-10 rounded-full bg-muted hover:bg-accent border border-border flex items-center justify-center shadow-lg transition-opacity",title:"Scroll to bottom",children:_.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:"text-muted-foreground",children:_.jsx("polyline",{points:"6 9 12 15 18 9"})})}),R&&_.jsx(Pb,{open:!0,onClose:()=>C(null),paths:R.paths,senderInfo:R.senderInfo,contacts:s,config:g??null})]})}const Lm=156,Lb=140,Mb=156,Ib=120,Ob=8,jb=D.forwardRef(function({onSend:s,disabled:i,placeholder:l,isRepeaterMode:c,conversationType:u,senderName:p},d){const[g,m]=D.useState(""),[x,A]=D.useState(!1),b=D.useRef(null);D.useImperativeHandle(d,()=>({appendText:M=>{var H;m(U=>U+M),(H=b.current)==null||H.focus()}}));const k=D.useMemo(()=>{if(u==="contact")return{warningAt:Lb,dangerAt:Lm,hardLimit:Lm};if(u==="channel"){const M=(p==null?void 0:p.length)??10,H=Math.max(1,Mb-M-2);return{warningAt:Ib,dangerAt:Math.max(1,H-Ob),hardLimit:H}}return null},[u,p]),{limitState:R,warningMessage:C}=D.useMemo(()=>{if(!k)return{limitState:"normal",warningMessage:null};const M=g.length;return M>=k.hardLimit?{limitState:"error",warningMessage:"likely truncated by radio"}:M>=k.dangerAt?{limitState:"danger",warningMessage:"may impact multi-repeater hop delivery"}:M>=k.warningAt?{limitState:"warning",warningMessage:"may impact multi-repeater hop delivery"}:{limitState:"normal",warningMessage:null}},[g.length,k]),w=k?k.hardLimit-g.length:0,S=D.useCallback(async M=>{M.preventDefault();const H=g.trim();if(c){if(x||i)return;A(!0);try{await s(H),m("")}catch(U){console.error("Failed to request telemetry:",U),xt.error("Failed to request telemetry",{description:U instanceof Error?U.message:"Check radio connection"});return}finally{A(!1)}setTimeout(()=>{var U;return(U=b.current)==null?void 0:U.focus()},0)}else{if(!H||x||i)return;A(!0);try{await s(H),m("")}catch(U){console.error("Failed to send message:",U),xt.error("Failed to send message",{description:U instanceof Error?U.message:"Check radio connection"});return}finally{A(!1)}setTimeout(()=>{var U;return(U=b.current)==null?void 0:U.focus()},0)}},[g,x,i,s,c]),P=D.useCallback(M=>{M.key==="Enter"&&!M.shiftKey&&(M.preventDefault(),S(M))},[S]),N=c?!0:g.trim().length>0,F=!c&&k!==null;return _.jsxs("form",{className:"px-4 py-3 border-t border-border flex flex-col gap-1",onSubmit:S,children:[_.jsxs("div",{className:"flex gap-2",children:[_.jsx(Xt,{ref:b,type:c?"password":"text",autoComplete:c?"off":void 0,value:g,onChange:M=>m(M.target.value),onKeyDown:P,placeholder:l||(c?"Enter password for admin login...":"Type a message..."),disabled:i||x,className:"flex-1 min-w-0"}),_.jsx(Jt,{type:"submit",disabled:i||x||!N,className:"flex-shrink-0",children:x?c?"Logging in...":"Sending...":c?g.trim()?"Log in with password":"Log in as guest/use repeater ACLs":"Send"})]}),F&&_.jsxs("div",{className:"flex items-center justify-end gap-2 text-xs",children:[_.jsxs("span",{className:dt("tabular-nums",R==="error"||R==="danger"?"text-red-500 font-medium":R==="warning"?"text-yellow-500":"text-muted-foreground"),children:[g.length,"/",k.hardLimit,w<0&&` (${w})`]}),C&&_.jsxs("span",{className:dt(R==="error"?"text-red-500":"text-yellow-500"),children:["— ",C]})]})]})});function Mm(n){const s=zb(n),i=D.forwardRef((l,c)=>{const{children:u,...p}=l,d=D.Children.toArray(u),g=d.find(Vb);if(g){const m=g.props.children,x=d.map(A=>A===g?D.Children.count(m)>1?D.Children.only(null):D.isValidElement(m)?m.props.children:null:A);return _.jsx(s,{...p,ref:c,children:D.isValidElement(m)?D.cloneElement(m,void 0,x):null})}return _.jsx(s,{...p,ref:c,children:u})});return i.displayName=`${n}.Slot`,i}function zb(n){const s=D.forwardRef((i,l)=>{const{children:c,...u}=i;if(D.isValidElement(c)){const p=Zb(c),d=Ub(u,c.props);return c.type!==D.Fragment&&(d.ref=l?El(l,p):p),D.cloneElement(c,d)}return D.Children.count(c)>1?D.Children.only(null):null});return s.displayName=`${n}.SlotClone`,s}var Hb=Symbol("radix.slottable");function Vb(n){return D.isValidElement(n)&&typeof n.type=="function"&&"__radixId"in n.type&&n.type.__radixId===Hb}function Ub(n,s){const i={...s};for(const l in s){const c=n[l],u=s[l];/^on[A-Z]/.test(l)?c&&u?i[l]=(...d)=>{const g=u(...d);return c(...d),g}:c&&(i[l]=c):l==="style"?i[l]={...c,...u}:l==="className"&&(i[l]=[c,u].filter(Boolean).join(" "))}return{...n,...i}}functio Line truncated
`).map(($,ue,pe)=>_.jsxs("span",{children:[Nb($,d),ue<pe.length-1&&_.jsx("br",{})]},ue)),!re&&_.jsxs(_.Fragment,{children:[_.jsx("span",{className:"text-[10px] text-muted-foreground/50 ml-2",children:ra(j.sender_timestamp||j.received_at)}),!j.outgoing&&j.paths&&j.paths.length>0&&_.jsx(Fm,{paths:j.paths,variant:"inline",onClick:()=>C({paths:j.paths,senderInfo:M(j,Z,ne)})})]}),j.outgoing&&(j.acked>0?` ✓${j.acked>1?j.acked:""}`:" ?")]})]})]},j.id)})]}),b&&_.jsx("button",{onClick:P,className:"absolute bottom-4 right-4 w-10 h-10 rounded-full bg-muted hover:bg-accent border border-border flex items-center justify-center shadow-lg transition-opacity",title:"Scroll to bottom",children:_.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:"text-muted-foreground",children:_.jsx("polyline",{points:"6 9 12 15 18 9"})})}),R&&_.jsx(Pb,{open:!0,onClose:()=>C(null),paths:R.paths,senderInfo:R.senderInfo,contacts:s,config:g??null})]})}const Lm=156,Lb=140,Mb=156,Ib=120,Ob=8,jb=D.forwardRef(function({onSend:s,disabled:i,placeholder:l,isRepeaterMode:c,conversationType:u,senderName:p},d){const[g,m]=D.useState(""),[x,A]=D.useState(!1),b=D.useRef(null);D.useImperativeHandle(d,()=>({appendText:M=>{var H;m(U=>U+M),(H=b.current)==null||H.focus()}}));const k=D.useMemo(()=>{if(u==="contact")return{warningAt:Lb,dangerAt:Lm,hardLimit:Lm};if(u==="channel"){const M=(p==null?void 0:p.length)??10,H=Math.max(1,Mb-M-2);return{warningAt:Ib,dangerAt:Math.max(1,H-Ob),hardLimit:H}}return null},[u,p]),{limitState:R,warningMessage:C}=D.useMemo(()=>{if(!k)return{limitState:"normal",warningMessage:null};const M=g.length;return M>=k.hardLimit?{limitState:"error",warningMessage:"likely truncated by radio"}:M>=k.dangerAt?{limitState:"danger",warningMessage:"may impact multi-repeater hop delivery"}:M>=k.warningAt?{limitState:"warning",warningMessage:"may impact multi-repeater hop delivery"}:{limitState:"normal",warningMessage:null}},[g.length,k]),w=k?k.hardLimit-g.length:0,S=D.useCallback(async M=>{M.preventDefault();const H=g.trim();if(c){if(x||i)return;A(!0);try{await s(H),m("")}catch(U){console.error("Failed to request telemetry:",U),xt.error("Failed to request telemetry",{description:U instanceof Error?U.message:"Check radio connection"});return}finally{A(!1)}setTimeout(()=>{var U;return(U=b.current)==null?void 0:U.focus()},0)}else{if(!H||x||i)return;A(!0);try{await s(H),m("")}catch(U){console.error("Failed to send message:",U),xt.error("Failed to send message",{description:U instanceof Error?U.message:"Check radio connection"});return}finally{A(!1)}setTimeout(()=>{var U;return(U=b.current)==null?void 0:U.focus()},0)}},[g,x,i,s,c]),P=D.useCallback(M=>{M.key==="Enter"&&!M.shiftKey&&(M.preventDefault(),S(M))},[S]),N=c?!0:g.trim().length>0,F=!c&&k!==null;return _.jsxs("form",{className:"px-4 py-3 border-t border-border flex flex-col gap-1",onSubmit:S,children:[_.jsxs("div",{className:"flex gap-2",children:[_.jsx(Xt,{ref:b,type:c?"password":"text",autoComplete:c?"off":void 0,value:g,onChange:M=>m(M.target.value),onKeyDown:P,placeholder:l||(c?"Enter password for admin login...":"Type a message..."),disabled:i||x,className:"flex-1 min-w-0"}),_.jsx(Jt,{type:"submit",disabled:i||x||!N,className:"flex-shrink-0",children:x?c?"Logging in...":"Sending...":c?g.trim()?"Log in with password":"Log in as guest/use repeater ACLs":"Send"})]}),F&&_.jsxs("div",{className:"flex items-center justify-end gap-2 text-xs",children:[_.jsxs("span",{className:dt("tabular-nums",R==="error"||R==="danger"?"text-red-500 font-medium":R==="warning"?"text-yellow-500":"text-muted-foreground"),children:[g.length,"/",k.hardLimit,w<0&&` (${w})`]}),C&&_.jsxs("span",{className:dt(R==="error"?"text-red-500":"text-yellow-500"),children:["— ",C]})]})]})});function Mm(n){const s=zb(n),i=D.forwardRef((l,c)=>{const{children:u,...p}=l,d=D.Children.toArray(u),g=d.find(Vb);if(g){const m=g.props.children,x=d.map(A=>A===g?D.Children.count(m)>1?D.Children.only(null):D.isValidElement(m)?m.props.children:null:A);return _.jsx(s,{...p,ref:c,children:D.isValidElement(m)?D.cloneElement(m,void 0,x):null})}return _.jsx(s,{...p,ref:c,children:u})});return i.displayName=`${n}.Slot`,i}function zb(n){const s=D.forwardRef((i,l)=>{const{children:c,...u}=i;if(D.isValidElement(c)){const p=Zb(c),d=Ub(u,c.props);return c.type!==D.Fragment&&(d.ref=l?El(l,p):p),D.cloneElement(c,d)}return D.Children.count(c)>1?D.Children.only(null):null});return s.displayName=`${n}.SlotClone`,s}var Hb=Symbol("radix.slottable");function Vb(n){return D.isValidElement(n)&&typeof n.type=="function"&&"__radixId"in n.type&&n.type.__radixId===Hb}function Ub(n,s){const i={...s};for(const l in s){const c=n[l],u=s[l];/^on[A-Z]/.test(l)?c&&u?i[l]=(...d)=>{const g=u(...d);return c(...d),g}:c&&(i[l]=c):l==="style"?i[l]={...c,...u}:l==="className"&&(i[l]=[c,u].filter(Boolean).join(" "))}return{...n,...i}}functio Line truncated
sender_name: str | None,
sender_key: str | None,
message_text: str,
@@ -153,7 +153,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${s.do
# Example: Only respond in #bot channel to "!pling" command
if channel_name == "#bot" and "!pling" in message_text.lower():
return "[BOT] Plong!"
return None`,[et,st]=D.useState([]),[Ge,$e]=D.useState(null),[Ye,Me]=D.useState(null),[wt,It]=D.useState("");D.useEffect(()=>{s&&(C(s.name),S(String(s.lat)),N(String(s.lon)),M(String(s.tx_power)),U(String(s.radio.freq)),ee(String(s.radio.bw)),Y(String(s.radio.sf)),K(String(s.radio.cr)))},[s]),D.useEffect(()=>{l&&(se(String(l.max_radio_contacts)),Ee(l.auto_decrypt_dm_on_advert),ot(String(l.advert_interval)),st(l.bots||[]))},[l]),D.useEffect(()=>{n&&A()},[n,A]);const Nn=D.useMemo(()=>{const oe=parseFloat(H),Te=parseFloat(j),pt=parseInt(Z,10),Ft=parseInt(ne,10);for(const nn of u0)if(nn.freq===oe&&nn.bw===Te&&nn.sf===pt&&nn.cr===Ft)return nn.name;return"custom"},[H,j,Z,ne]),Ot=oe=>{if(oe==="custom")return;const Te=u0.find(pt=>pt.name===oe);Te&&(U(String(Te.freq)),ee(String(Te.bw)),Y(String(Te.sf)),K(String(Te.cr)))},en=()=>{if(!navigator.geolocation){xt.error("Geolocation not supported",{description:"Your browser does not support geolocation"});return}ue(!0),navigator.geolocation.getCurrentPosition(oe=>{S(oe.coords.latitude.toFixed(6)),N(oe.coords.longitude.toFixed(6)),ue(!1),xt.success("Location updated")},oe=>{ue(!1),xt.error("Failed to get location",{description:oe.message})},{enableHighAccuracy:!0,timeout:1e4})},sn=async()=>{Ae(""),re(!0);try{const oe={lat:parseFloat(w),lon:parseFloat(P),tx_power:parseInt(F,10),radio:{freq:parseFloat(H),bw:parseFloat(j),sf:parseInt(Z,10),cr:parseInt(ne,10)}};await u(oe),xt.success("Radio config saved, rebooting..."),re(!1),T(!0),await g(),c()}catch(oe){Ae(oe instanceof Error?oe.message:"Failed to save"),re(!1)}finally{T(!1)}},yn=async()=>{Ae(""),re(!0);try{await u({name:R});const Te=parseInt(Ue,10);!isNaN(Te)&&Te!==(l==null?void 0:l.advert_interval)&&await p({advert_interval:Te}),xt.success("Identity settings saved")}catch(oe){Ae(oe instanceof Error?oe.message:"Failed to save")}finally{re(!1)}},Ht=async()=>{Ae(""),re(!0);try{const oe=parseInt(G,10);!isNaN(oe)&&oe!==(l==null?void 0:l.max_radio_contacts)&&await p({max_radio_contacts:oe}),xt.success("Serial settings saved")}catch(oe){Ae(oe instanceof Error?oe.message:"Failed to save")}finally{re(!1)}},Pt=async()=>{if(!ae.trim()){Ae("Private key is required");return}Ae(""),re(!0);try{await d(ae.trim()),xe(""),xt.success("Private key set, rebooting..."),re(!1),T(!0),await g(),c()}catch(oe){Ae(oe instanceof Error?oe.message:"Failed to set private key"),re(!1)}finally{T(!1)}},Vt=async()=>{if(confirm("Are you sure you want to reboot the radio? The connection will drop temporarily.")){Ae(""),T(!0);try{await g(),c()}catch(oe){Ae(oe instanceof Error?oe.message:"Failed to reboot radio")}finally{T(!1)}}},Fn=async()=>{q(!0);try{await m()}finally{q(!1)}},pn=async()=>{const oe=parseInt(me,10);if(isNaN(oe)||oe<1){xt.error("Invalid retention days",{description:"Retention days must be at least 1"});return}We(!0);try{const Te=await rt.runMaintenance(oe);xt.success("Database cleanup complete",{description:`Deleted ${Te.packets_deleted} old packet${Te.packets_deleted===1?"":"s"}`}),await x()}catch(Te){console.error("Failed to run maintenance:",Te),xt.error("Database cleanup failed",{description:Te instanceof Error?Te.message:"Unknown error"})}finally{We(!1)}},ye=async()=>{re(!0),Ae("");try{await p({auto_decrypt_dm_on_advert:Ke}),xt.success("Database settings saved")}catch(oe){console.error("Failed to save database settings:",oe),Ae(oe instanceof Error?oe.message:"Failed to save"),xt.error("Failed to save settings")}finally{re(!1)}},ke=async()=>{re(!0),Ae("");try{await p({bots:et}),xt.success("Bot settings saved")}catch(oe){console.error("Failed to save bot settings:",oe);const Te=oe instanceof Error?oe.message:"Failed to save";Ae(Te),xt.error(Te)}finally{re(!1)}},Fe=()=>{const oe={id:crypto.randomUUID(),name:`Bot ${et.length+1}`,enabled:!1,code:ht};st([...et,oe]),$e(oe.id)},lt=oe=>{const Te=et.find(pt=>pt.id===oe);Te&&Te.code.trim()&&Te.code!==ht&&!confirm(`Delete "${Te.name}"? This will remove all its code.`)||(st(et.filter(pt=>pt.id!==oe)),Ge===oe&&$e(null))},te=oe=>{st(et.map(Te=>Te.id===oe?{...Te,enabled:!Te.enabled}:Te))},ve=(oe,Te)=>{st(et.map(pt=>pt.id===oe?{...pt,code:Te}:pt))},_e=oe=>{Me(oe.id),It(oe.name)},Re=()=>{Ye&&wt.trim()&&st(et.map(oe=>oe.id===Ye?{...oe,name:wt.trim()}:oe)),Me(null),It("")},ct=oe=>{st(et.map(Te=>Te.id===oe?{...Te,code:ht}:Te))};return _.jsx(Z0,{open:n,onOpenChange:oe=>!oe&&c(),children:_.jsxs(ud,{className:"sm:max-w-[50vw] sm:min-w-[500px] max-h-[90vh] overflow-y-auto",children:[_.jsxs(dd,{children:[_.jsx(fd,{children:"Radio & Settings"}),_.jsxs(hd,{className:"sr-only",children:[b==="radio"&&"Configure radio frequency, power, and location settings",b==="identity"&&"Manage radio name, public key, private key, and advertising settings",b==="serial"&&"View serial port connection and configure contact sync",b==="database"&&"View database statistics and clean up old packets",b==="bot"&&"Configure automatic message bot with Python code"]})]}),s?_.jsxs(G0,{value:b,onValueChange:oe=>k(oe),className:"w-full",childre Line truncated
return None`,[et,st]=D.useState([]),[Ge,$e]=D.useState(null),[Ye,Me]=D.useState(null),[wt,It]=D.useState("");D.useEffect(()=>{s&&(C(s.name),S(String(s.lat)),N(String(s.lon)),M(String(s.tx_power)),U(String(s.radio.freq)),ee(String(s.radio.bw)),Y(String(s.radio.sf)),K(String(s.radio.cr)))},[s]),D.useEffect(()=>{l&&(se(String(l.max_radio_contacts)),Ee(l.auto_decrypt_dm_on_advert),ot(String(l.advert_interval)),st(l.bots||[]))},[l]),D.useEffect(()=>{n&&A()},[n,A]);const Nn=D.useMemo(()=>{const oe=parseFloat(H),Te=parseFloat(j),pt=parseInt(Z,10),Ft=parseInt(ne,10);for(const nn of u0)if(nn.freq===oe&&nn.bw===Te&&nn.sf===pt&&nn.cr===Ft)return nn.name;return"custom"},[H,j,Z,ne]),Ot=oe=>{if(oe==="custom")return;const Te=u0.find(pt=>pt.name===oe);Te&&(U(String(Te.freq)),ee(String(Te.bw)),Y(String(Te.sf)),K(String(Te.cr)))},en=()=>{if(!navigator.geolocation){xt.error("Geolocation not supported",{description:"Your browser does not support geolocation"});return}ue(!0),navigator.geolocation.getCurrentPosition(oe=>{S(oe.coords.latitude.toFixed(6)),N(oe.coords.longitude.toFixed(6)),ue(!1),xt.success("Location updated")},oe=>{ue(!1),xt.error("Failed to get location",{description:oe.message})},{enableHighAccuracy:!0,timeout:1e4})},sn=async()=>{Ae(""),re(!0);try{const oe={lat:parseFloat(w),lon:parseFloat(P),tx_power:parseInt(F,10),radio:{freq:parseFloat(H),bw:parseFloat(j),sf:parseInt(Z,10),cr:parseInt(ne,10)}};await u(oe),xt.success("Radio config saved, rebooting..."),re(!1),T(!0),await g(),c()}catch(oe){Ae(oe instanceof Error?oe.message:"Failed to save"),re(!1)}finally{T(!1)}},yn=async()=>{Ae(""),re(!0);try{await u({name:R});const Te=parseInt(Ue,10);!isNaN(Te)&&Te!==(l==null?void 0:l.advert_interval)&&await p({advert_interval:Te}),xt.success("Identity settings saved")}catch(oe){Ae(oe instanceof Error?oe.message:"Failed to save")}finally{re(!1)}},Ht=async()=>{Ae(""),re(!0);try{const oe=parseInt(G,10);!isNaN(oe)&&oe!==(l==null?void 0:l.max_radio_contacts)&&await p({max_radio_contacts:oe}),xt.success("Connectivity settings saved")}catch(oe){Ae(oe instanceof Error?oe.message:"Failed to save")}finally{re(!1)}},Pt=async()=>{if(!ae.trim()){Ae("Private key is required");return}Ae(""),re(!0);try{await d(ae.trim()),xe(""),xt.success("Private key set, rebooting..."),re(!1),T(!0),await g(),c()}catch(oe){Ae(oe instanceof Error?oe.message:"Failed to set private key"),re(!1)}finally{T(!1)}},Vt=async()=>{if(confirm("Are you sure you want to reboot the radio? The connection will drop temporarily.")){Ae(""),T(!0);try{await g(),c()}catch(oe){Ae(oe instanceof Error?oe.message:"Failed to reboot radio")}finally{T(!1)}}},Fn=async()=>{q(!0);try{await m()}finally{q(!1)}},pn=async()=>{const oe=parseInt(me,10);if(isNaN(oe)||oe<1){xt.error("Invalid retention days",{description:"Retention days must be at least 1"});return}We(!0);try{const Te=await rt.runMaintenance(oe);xt.success("Database cleanup complete",{description:`Deleted ${Te.packets_deleted} old packet${Te.packets_deleted===1?"":"s"}`}),await x()}catch(Te){console.error("Failed to run maintenance:",Te),xt.error("Database cleanup failed",{description:Te instanceof Error?Te.message:"Unknown error"})}finally{We(!1)}},ye=async()=>{re(!0),Ae("");try{await p({auto_decrypt_dm_on_advert:Ke}),xt.success("Database settings saved")}catch(oe){console.error("Failed to save database settings:",oe),Ae(oe instanceof Error?oe.message:"Failed to save"),xt.error("Failed to save settings")}finally{re(!1)}},ke=async()=>{re(!0),Ae("");try{await p({bots:et}),xt.success("Bot settings saved")}catch(oe){console.error("Failed to save bot settings:",oe);const Te=oe instanceof Error?oe.message:"Failed to save";Ae(Te),xt.error(Te)}finally{re(!1)}},Fe=()=>{const oe={id:crypto.randomUUID(),name:`Bot ${et.length+1}`,enabled:!1,code:ht};st([...et,oe]),$e(oe.id)},lt=oe=>{const Te=et.find(pt=>pt.id===oe);Te&&Te.code.trim()&&Te.code!==ht&&!confirm(`Delete "${Te.name}"? This will remove all its code.`)||(st(et.filter(pt=>pt.id!==oe)),Ge===oe&&$e(null))},te=oe=>{st(et.map(Te=>Te.id===oe?{...Te,enabled:!Te.enabled}:Te))},ve=(oe,Te)=>{st(et.map(pt=>pt.id===oe?{...pt,code:Te}:pt))},_e=oe=>{Me(oe.id),It(oe.name)},Re=()=>{Ye&&wt.trim()&&st(et.map(oe=>oe.id===Ye?{...oe,name:wt.trim()}:oe)),Me(null),It("")},ct=oe=>{st(et.map(Te=>Te.id===oe?{...Te,code:ht}:Te))};return _.jsx(Z0,{open:n,onOpenChange:oe=>!oe&&c(),children:_.jsxs(ud,{className:"sm:max-w-[50vw] sm:min-w-[500px] max-h-[90vh] overflow-y-auto",children:[_.jsxs(dd,{children:[_.jsx(fd,{children:"Radio & Settings"}),_.jsxs(hd,{className:"sr-only",children:[b==="radio"&&"Configure radio frequency, power, and location settings",b==="identity"&&"Manage radio name, public key, private key, and advertising settings",b==="connectivity"&&"View connection status and configure contact sync",b==="database"&&"View database statistics and clean up old packets",b==="bot"&&"Configure automatic message bot with Python code"]})]}),s?_.jsxs(G0,{value:b,onValueChange:oe=>k(oe),className:"w-full", Line truncated
(c) 2012 by Cédric Mesnil. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
@@ -568,5 +568,5 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
See https://github.com/richtr/NoSleep.js/issues/15 for more details.
`),this.noSleepTimer=window.setInterval(()=>{document.hidden||(window.location.href=window.location.href.split("#")[0],window.setTimeout(window.stop,0))},15e3),this.enabled=!0,Promise.resolve()):this.noSleepVideo.play().then(d=>(this.enabled=!0,d)).catch(d=>{throw this.enabled=!1,d})}disable(){l()?(this._wakeLock&&this._wakeLock.release(),this._wakeLock=null):i()?this.noSleepTimer&&(console.warn(`
NoSleep now disabled for older iOS devices.
`),window.clearInterval(this.noSleepTimer),this.noSleepTimer=null):this.noSleepVideo.pause(),this.enabled=!1}}return m0=c,m0}var dS=uS();const fS=aa(dS);function hS(n){if(n.length<4)return null;try{const i=parseInt(n.slice(0,2),16)&3;let l=2;if(i===0||i===3){if(n.length<l+8)return null;l+=8}if(n.length<l+2)return null;const c=parseInt(n.slice(l,l+2),16);l+=2;const u=c*2;return n.length<l+u?null:(l+=u,n.slice(l))}catch{return null}}function pS({packets:n,channels:s,onChannelCreate:i,onRunningChange:l,visible:c=!1}){const[u,p]=D.useState(!1),[d,g]=D.useState(6),[m,x]=D.useState(!1),[A,b]=D.useState(!0),[k,R]=D.useState(!1),[C,w]=D.useState(null),[S,P]=D.useState(new Map),[N,F]=D.useState([]),[M,H]=D.useState(!1),[U,j]=D.useState(null),[ee,Z]=D.useState(null),[Y,ne]=D.useState(0),K=D.useRef(null),ae=D.useRef(null),xe=D.useRef(!1),G=D.useRef(!1),se=D.useRef(!1),W=D.useRef(new Map),re=D.useRef(!1),V=D.useRef(6),T=D.useRef(!0),z=D.useRef(!1),q=D.useRef(new Set),$=D.useRef(new Set);D.useEffect(()=>{const Ee=new lS;K.current=Ee,j(Ee.isGpuAvailable());const Ue=new fS;return ae.current=Ue,()=>{Ee.destroy(),K.current=null,Ue.disable(),ae.current=null}},[]),D.useEffect(()=>{!c||M||KA(async()=>{const{ENGLISH_WORDLIST:Ee}=await import("./wordlist-BtmChKSf.js");return{ENGLISH_WORDLIST:Ee}},[]).then(({ENGLISH_WORDLIST:Ee})=>{K.current&&(K.current.setWordlist(Ee),H(!0))}).catch(Ee=>{console.error("Failed to load wordlist:",Ee),xt.error("Failed to load wordlist",{description:"Cracking will not be available"})})},[c,M]),D.useEffect(()=>{const Ee=()=>{rt.getUndecryptedPacketCount().then(({count:ot})=>Z(ot)).catch(()=>Z(null))};Ee();const Ue=setInterval(Ee,36e5);return()=>clearInterval(Ue)},[]);const ue=D.useMemo(()=>new Set(s.map(Ee=>Ee.key.toUpperCase())),[s]),pe=n.filter(Ee=>Ee.payload_type==="GROUP_TEXT"&&!Ee.decrypted);D.useEffect(()=>{let Ee=0;P(Ue=>{const ot=new Map(Ue);let ht=!1;for(const et of pe)if(!ot.has(et.id)){const st=hS(et.data);if(st&&$.current.has(st)){Ee++;continue}st&&$.current.add(st),ot.set(et.id,{packet:et,attempts:0,lastAttemptLength:0,status:"pending"}),ht=!0}return ht?(W.current=ot,ot):Ue}),Ee>0&&ne(Ue=>Ue+Ee)},[pe.length]),D.useEffect(()=>{W.current=S},[S]),D.useEffect(()=>{re.current=m},[m]),D.useEffect(()=>{V.current=d},[d]),D.useEffect(()=>{T.current=A},[A]),D.useEffect(()=>{z.current=k},[k]),D.useEffect(()=>{q.current=new Set(pe.map(Ee=>Ee.id))},[pe]),D.useEffect(()=>{l==null||l(u)},[u,l]);const Ae=Array.from(S.values()).filter(Ee=>Ee.status==="pending").length,me=Array.from(S.values()).filter(Ee=>Ee.status==="cracked").length,ge=Array.from(S.values()).filter(Ee=>Ee.status==="failed").length,Be=D.useCallback(async()=>{if(se.current||!K.current||!xe.current)return;const Ee=W.current;let Ue=null,ot=null;for(const[Ge,$e]of Ee.entries())if($e.status==="pending"){Ue=$e,ot=Ge;break}if(!Ue&&re.current){const Ge=Array.from(Ee.entries()).filter(([,$e])=>$e.status==="failed"&&$e.lastAttemptLength<10);Ge.length>0&&(Ge.sort(($e,Ye)=>$e[1].lastAttemptLength-Ye[1].lastAttemptLength),[ot,Ue]=Ge[0])}if(!Ue||ot===null){xe.current&&setTimeout(()=>Be(),1e3);return}if(!q.current.has(ot)){P(Ge=>{const $e=new Map(Ge);return $e.delete(ot),$e}),xe.current&&setTimeout(()=>Be(),10);return}se.current=!0;const ht=V.current,et=Ue.lastAttemptLength>0,st=et?Ue.lastAttemptLength+1:ht;try{const Ge=await K.current.crack(Ue.packet.data,{maxLength:st,useSenderFilter:!0,useTimestampFilter:!0,useUtf8Filter:!0,...z.current&&{gpuDispatchMs:1e4},...et&&{useDictionary:!1,startingLength:st}},$e=>{w($e)});if(G.current){G.current=!1,se.current=!1,w(null);return}if(Ge.found&&Ge.roomName&&Ge.key){P(Me=>{const wt=new Map(Me),It=wt.get(ot);return It&&wt.set(ot,{...It,status:"cracked",attempts:It.attempts+1,lastAttemptLength:st}),wt});const $e={roomName:Ge.roomName,key:Ge.key,packetId:ot,message:Ge.decryptedMessage||"",crackedAt:Date.now()};F(Me=>[...Me,$e]);const Ye=Ge.key.toUpperCase();if(!ue.has(Ye))try{const Me="#"+Ge.roomName;await i(Me,Ge.key),T.current&&await rt.decryptHistoricalPackets({key_type:"channel",channel_name:Me})}catch(Me){console.error("Failed to create channel or decrypt historical:",Me),xt.error("Failed to save cracked channel",{description:Me instanceof Error?Me.message:"Channel discovered but could not be saved"})}}else P($e=>{const Ye=new Map($e),Me=Ye.get(ot);return Me&&Ye.set(ot,{...Me,status:"failed",attempts:Me.attempts+1,lastAttemptLength:st}),Ye})}catch(Ge){console.error("Cracking error:",Ge),P($e=>{const Ye=new Map($e),Me=Ye.get(ot);return Me&&Ye.set(ot,{...Me,status:"failed",attempts:Me.attempts+1,lastAttemptLength:st}),Ye})}se.current=!1,w(null),xe.current&&setTimeout(()=>Be(),100)},[ue,i]),We=()=>{var Ee;if(!U){xt.error("WebGPU not available",{description:"Cracking requires Chrome 113+ or Edge 113+ with WebGPU support."});return}p(!0),xe.current=!0,G.current=!1,(Ee=ae.current)==null||Ee.enable(),Be()},Ke=()=>{var Ee,Ue;p(!1),xe.current=!1,G.current=!0,(Ee=K.current)==null||Ee.abort(),(Ue=ae.current)==nul Line truncated
//# sourceMappingURL=index-CWnjp-zX.js.map
`),window.clearInterval(this.noSleepTimer),this.noSleepTimer=null):this.noSleepVideo.pause(),this.enabled=!1}}return m0=c,m0}var dS=uS();const fS=aa(dS);function hS(n){if(n.length<4)return null;try{const i=parseInt(n.slice(0,2),16)&3;let l=2;if(i===0||i===3){if(n.length<l+8)return null;l+=8}if(n.length<l+2)return null;const c=parseInt(n.slice(l,l+2),16);l+=2;const u=c*2;return n.length<l+u?null:(l+=u,n.slice(l))}catch{return null}}function pS({packets:n,channels:s,onChannelCreate:i,onRunningChange:l,visible:c=!1}){const[u,p]=D.useState(!1),[d,g]=D.useState(6),[m,x]=D.useState(!1),[A,b]=D.useState(!0),[k,R]=D.useState(!1),[C,w]=D.useState(null),[S,P]=D.useState(new Map),[N,F]=D.useState([]),[M,H]=D.useState(!1),[U,j]=D.useState(null),[ee,Z]=D.useState(null),[Y,ne]=D.useState(0),K=D.useRef(null),ae=D.useRef(null),xe=D.useRef(!1),G=D.useRef(!1),se=D.useRef(!1),W=D.useRef(new Map),re=D.useRef(!1),V=D.useRef(6),T=D.useRef(!0),z=D.useRef(!1),q=D.useRef(new Set),$=D.useRef(new Set);D.useEffect(()=>{const Ee=new lS;K.current=Ee,j(Ee.isGpuAvailable());const Ue=new fS;return ae.current=Ue,()=>{Ee.destroy(),K.current=null,Ue.disable(),ae.current=null}},[]),D.useEffect(()=>{!c||M||KA(async()=>{const{ENGLISH_WORDLIST:Ee}=await import("./wordlist-BtmChKSf.js");return{ENGLISH_WORDLIST:Ee}},[]).then(({ENGLISH_WORDLIST:Ee})=>{K.current&&(K.current.setWordlist(Ee),H(!0))}).catch(Ee=>{console.error("Failed to load wordlist:",Ee),xt.error("Failed to load wordlist",{description:"Cracking will not be available"})})},[c,M]),D.useEffect(()=>{const Ee=()=>{rt.getUndecryptedPacketCount().then(({count:ot})=>Z(ot)).catch(()=>Z(null))};Ee();const Ue=setInterval(Ee,36e5);return()=>clearInterval(Ue)},[]);const ue=D.useMemo(()=>new Set(s.map(Ee=>Ee.key.toUpperCase())),[s]),pe=n.filter(Ee=>Ee.payload_type==="GROUP_TEXT"&&!Ee.decrypted);D.useEffect(()=>{let Ee=0;P(Ue=>{const ot=new Map(Ue);let ht=!1;for(const et of pe)if(!ot.has(et.id)){const st=hS(et.data);if(st&&$.current.has(st)){Ee++;continue}st&&$.current.add(st),ot.set(et.id,{packet:et,attempts:0,lastAttemptLength:0,status:"pending"}),ht=!0}return ht?(W.current=ot,ot):Ue}),Ee>0&&ne(Ue=>Ue+Ee)},[pe.length]),D.useEffect(()=>{W.current=S},[S]),D.useEffect(()=>{re.current=m},[m]),D.useEffect(()=>{V.current=d},[d]),D.useEffect(()=>{T.current=A},[A]),D.useEffect(()=>{z.current=k},[k]),D.useEffect(()=>{q.current=new Set(pe.map(Ee=>Ee.id))},[pe]),D.useEffect(()=>{l==null||l(u)},[u,l]);const Ae=Array.from(S.values()).filter(Ee=>Ee.status==="pending").length,me=Array.from(S.values()).filter(Ee=>Ee.status==="cracked").length,ge=Array.from(S.values()).filter(Ee=>Ee.status==="failed").length,Be=D.useCallback(async()=>{if(se.current||!K.current||!xe.current)return;const Ee=W.current;let Ue=null,ot=null;for(const[Ge,$e]of Ee.entries())if($e.status==="pending"){Ue=$e,ot=Ge;break}if(!Ue&&re.current){const Ge=Array.from(Ee.entries()).filter(([,$e])=>$e.status==="failed"&&$e.lastAttemptLength<10);Ge.length>0&&(Ge.sort(($e,Ye)=>$e[1].lastAttemptLength-Ye[1].lastAttemptLength),[ot,Ue]=Ge[0])}if(!Ue||ot===null){xe.current&&setTimeout(()=>Be(),1e3);return}if(!q.current.has(ot)){P(Ge=>{const $e=new Map(Ge);return $e.delete(ot),$e}),xe.current&&setTimeout(()=>Be(),10);return}se.current=!0;const ht=V.current,et=Ue.lastAttemptLength>0,st=et?Ue.lastAttemptLength+1:ht;try{const Ge=await K.current.crack(Ue.packet.data,{maxLength:st,useSenderFilter:!0,useTimestampFilter:!0,useUtf8Filter:!0,...z.current&&{gpuDispatchMs:1e4},...et&&{useDictionary:!1,startingLength:st}},$e=>{w($e)});if(G.current){G.current=!1,se.current=!1,w(null);return}if(Ge.found&&Ge.roomName&&Ge.key){P(Me=>{const wt=new Map(Me),It=wt.get(ot);return It&&wt.set(ot,{...It,status:"cracked",attempts:It.attempts+1,lastAttemptLength:st}),wt});const $e={roomName:Ge.roomName,key:Ge.key,packetId:ot,message:Ge.decryptedMessage||"",crackedAt:Date.now()};F(Me=>[...Me,$e]);const Ye=Ge.key.toUpperCase();if(!ue.has(Ye))try{const Me="#"+Ge.roomName;await i(Me,Ge.key),T.current&&await rt.decryptHistoricalPackets({key_type:"channel",channel_name:Me})}catch(Me){console.error("Failed to create channel or decrypt historical:",Me),xt.error("Failed to save cracked channel",{description:Me instanceof Error?Me.message:"Channel discovered but could not be saved"})}}else P($e=>{const Ye=new Map($e),Me=Ye.get(ot);return Me&&Ye.set(ot,{...Me,status:"failed",attempts:Me.attempts+1,lastAttemptLength:st}),Ye})}catch(Ge){console.error("Cracking error:",Ge),P($e=>{const Ye=new Map($e),Me=Ye.get(ot);return Me&&Ye.set(ot,{...Me,status:"failed",attempts:Me.attempts+1,lastAttemptLength:st}),Ye})}se.current=!1,w(null),xe.current&&setTimeout(()=>Be(),100)},[ue,i]),We=()=>{var Ee;if(!U){xt.error("WebGPU not available",{description:"Cracking requires Chrome 113+ or Edge 113+ with WebGPU support."});return}p(!0),xe.current=!0,G.current=!1,(Ee=ae.current)==null||Ee.enable(),Be()},Ke=()=>{var Ee,Ue;p(!1),xe.current=!1,G.current=!0,(Ee=K.current)==null||Ee.abort(),(Ue=ae.current)==nul Line truncated
//# sourceMappingURL=index-BteQsTFF.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"mappings":"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 truncated
-1
View File
@@ -1 +0,0 @@
{"version":3,"mappings":"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 truncated
+1 -1
View File
@@ -13,7 +13,7 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<script type="module" crossorigin src="/assets/index-CWnjp-zX.js"></script>
<script type="module" crossorigin src="/assets/index-BteQsTFF.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DJA5wYVF.css">
</head>
<body>
+3 -1
View File
@@ -135,7 +135,9 @@ export function App() {
if (prev !== null && prev.radio_connected !== data.radio_connected) {
if (data.radio_connected) {
toast.success('Radio connected', {
description: data.serial_port ? `Connected to ${data.serial_port}` : undefined,
description: data.connection_info
? `Connected via ${data.connection_info}`
: undefined,
});
// Refresh config after reconnection (may have changed after reboot)
api.getRadioConfig().then(setConfig).catch(console.error);
+11 -11
View File
@@ -78,7 +78,7 @@ export function SettingsModal({
onRefreshAppSettings,
}: SettingsModalProps) {
// Tab state
type SettingsTab = 'radio' | 'identity' | 'serial' | 'database' | 'bot';
type SettingsTab = 'radio' | 'identity' | 'connectivity' | 'database' | 'bot';
const [activeTab, setActiveTab] = useState<SettingsTab>('radio');
// Radio config state
@@ -285,7 +285,7 @@ export function SettingsModal({
}
};
const handleSaveSerial = async () => {
const handleSaveConnectivity = async () => {
setError('');
setLoading(true);
@@ -294,7 +294,7 @@ export function SettingsModal({
if (!isNaN(newMaxRadioContacts) && newMaxRadioContacts !== appSettings?.max_radio_contacts) {
await onSaveAppSettings({ max_radio_contacts: newMaxRadioContacts });
}
toast.success('Serial settings saved');
toast.success('Connectivity settings saved');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save');
} finally {
@@ -474,7 +474,7 @@ export function SettingsModal({
{activeTab === 'radio' && 'Configure radio frequency, power, and location settings'}
{activeTab === 'identity' &&
'Manage radio name, public key, private key, and advertising settings'}
{activeTab === 'serial' && 'View serial port connection and configure contact sync'}
{activeTab === 'connectivity' && 'View connection status and configure contact sync'}
{activeTab === 'database' && 'View database statistics and clean up old packets'}
{activeTab === 'bot' && 'Configure automatic message bot with Python code'}
</DialogDescription>
@@ -491,7 +491,7 @@ export function SettingsModal({
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="radio">Radio</TabsTrigger>
<TabsTrigger value="identity">Identity</TabsTrigger>
<TabsTrigger value="serial">Serial</TabsTrigger>
<TabsTrigger value="connectivity">Connectivity</TabsTrigger>
<TabsTrigger value="database">Database</TabsTrigger>
<TabsTrigger value="bot">Bot</TabsTrigger>
</TabsList>
@@ -716,15 +716,15 @@ export function SettingsModal({
{error && <div className="text-sm text-destructive">{error}</div>}
</TabsContent>
{/* Serial Tab */}
<TabsContent value="serial" className="space-y-4 mt-4">
{/* Connectivity Tab */}
<TabsContent value="connectivity" className="space-y-4 mt-4">
<div className="space-y-2">
<Label>Serial Port</Label>
{health?.serial_port ? (
<Label>Connection</Label>
{health?.connection_info ? (
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" />
<code className="px-2 py-1 bg-muted rounded text-foreground text-sm">
{health.serial_port}
{health.connection_info}
</code>
</div>
) : (
@@ -752,7 +752,7 @@ export function SettingsModal({
</p>
</div>
<Button onClick={handleSaveSerial} disabled={loading} className="w-full">
<Button onClick={handleSaveConnectivity} disabled={loading} className="w-full">
{loading ? 'Saving...' : 'Save Settings'}
</Button>
+2 -2
View File
@@ -70,7 +70,7 @@ describe('parseWebSocketMessage', () => {
const onHealth = vi.fn();
const data = JSON.stringify({
type: 'health',
data: { radio_connected: true, serial_port: '/dev/ttyUSB0' },
data: { radio_connected: true, connection_info: 'Serial: /dev/ttyUSB0' },
});
const result = parseWebSocketMessage(data, { onHealth });
@@ -79,7 +79,7 @@ describe('parseWebSocketMessage', () => {
expect(result.handled).toBe(true);
expect(onHealth).toHaveBeenCalledWith({
radio_connected: true,
serial_port: '/dev/ttyUSB0',
connection_info: 'Serial: /dev/ttyUSB0',
});
});
+1 -1
View File
@@ -26,7 +26,7 @@ export interface RadioConfigUpdate {
export interface HealthStatus {
status: string;
radio_connected: boolean;
serial_port: string | null;
connection_info: string | null;
database_size_mb: number;
oldest_undecrypted_timestamp: number | null;
}
+2 -2
View File
@@ -14,7 +14,7 @@ export default async function globalSetup(_config: FullConfig) {
if (!res.ok) {
throw new Error(`Health check returned ${res.status}`);
}
const health = (await res.json()) as { radio_connected: boolean; serial_port: string | null };
const health = (await res.json()) as { radio_connected: boolean; connection_info: string | null };
if (!health.radio_connected) {
throw new Error(
@@ -23,7 +23,7 @@ export default async function globalSetup(_config: FullConfig) {
);
}
console.log(`Radio connected on ${health.serial_port}`);
console.log(`Radio connected on ${health.connection_info}`);
return;
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err));
+1 -1
View File
@@ -21,7 +21,7 @@ async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
export interface HealthStatus {
radio_connected: boolean;
serial_port: string | null;
connection_info: string | null;
}
export function getHealth(): Promise<HealthStatus> {
+8 -8
View File
@@ -18,7 +18,7 @@ class TestHealthEndpoint:
with patch("app.routers.health.radio_manager") as mock_rm:
mock_rm.is_connected = True
mock_rm.port = "/dev/ttyUSB0"
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
from app.main import app
@@ -29,7 +29,7 @@ class TestHealthEndpoint:
assert response.status_code == 200
data = response.json()
assert data["radio_connected"] is True
assert data["serial_port"] == "/dev/ttyUSB0"
assert data["connection_info"] == "Serial: /dev/ttyUSB0"
def test_health_disconnected_state(self):
"""Health endpoint reflects disconnected radio."""
@@ -37,7 +37,7 @@ class TestHealthEndpoint:
with patch("app.routers.health.radio_manager") as mock_rm:
mock_rm.is_connected = False
mock_rm.port = None
mock_rm.connection_info = None
from app.main import app
@@ -48,7 +48,7 @@ class TestHealthEndpoint:
assert response.status_code == 200
data = response.json()
assert data["radio_connected"] is False
assert data["serial_port"] is None
assert data["connection_info"] is None
class TestMessagesEndpoint:
@@ -1252,7 +1252,7 @@ class TestHealthEndpointDatabaseSize:
patch("app.routers.health.os.path.getsize") as mock_getsize,
):
mock_rm.is_connected = True
mock_rm.port = "/dev/ttyUSB0"
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_getsize.return_value = 10 * 1024 * 1024 # 10 MB
from app.main import app
@@ -1282,7 +1282,7 @@ class TestHealthEndpointOldestUndecrypted:
patch("app.routers.health.RawPacketRepository") as mock_repo,
):
mock_rm.is_connected = True
mock_rm.port = "/dev/ttyUSB0"
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_getsize.return_value = 5 * 1024 * 1024 # 5 MB
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=1700000000)
@@ -1309,7 +1309,7 @@ class TestHealthEndpointOldestUndecrypted:
patch("app.routers.health.RawPacketRepository") as mock_repo,
):
mock_rm.is_connected = True
mock_rm.port = "/dev/ttyUSB0"
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_getsize.return_value = 1 * 1024 * 1024 # 1 MB
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
@@ -1336,7 +1336,7 @@ class TestHealthEndpointOldestUndecrypted:
patch("app.routers.health.RawPacketRepository") as mock_repo,
):
mock_rm.is_connected = False
mock_rm.port = None
mock_rm.connection_info = None
mock_getsize.side_effect = OSError("File not found")
mock_repo.get_oldest_undecrypted = AsyncMock(side_effect=RuntimeError("No DB"))
+82
View File
@@ -0,0 +1,82 @@
"""Tests for configuration validation.
These tests verify transport mutual exclusivity, BLE PIN requirement,
and connection_type derivation.
"""
import pytest
from pydantic import ValidationError
from app.config import Settings
class TestTransportExclusivity:
"""Ensure only one transport can be configured at a time."""
def test_no_transport_defaults_to_serial(self):
"""No transport env vars means serial auto-detect."""
s = Settings(serial_port="", tcp_host="", ble_address="")
assert s.connection_type == "serial"
def test_serial_only(self):
s = Settings(serial_port="/dev/ttyUSB0")
assert s.connection_type == "serial"
def test_tcp_only(self):
s = Settings(tcp_host="192.168.1.1")
assert s.connection_type == "tcp"
def test_tcp_with_custom_port(self):
s = Settings(tcp_host="192.168.1.1", tcp_port=5000)
assert s.connection_type == "tcp"
assert s.tcp_port == 5000
def test_tcp_default_port(self):
s = Settings(tcp_host="192.168.1.1")
assert s.tcp_port == 4000
def test_ble_only(self):
s = Settings(ble_address="AA:BB:CC:DD:EE:FF", ble_pin="123456")
assert s.connection_type == "ble"
def test_serial_and_tcp_rejected(self):
with pytest.raises(ValidationError, match="Only one transport"):
Settings(serial_port="/dev/ttyUSB0", tcp_host="192.168.1.1")
def test_serial_and_ble_rejected(self):
with pytest.raises(ValidationError, match="Only one transport"):
Settings(
serial_port="/dev/ttyUSB0",
ble_address="AA:BB:CC:DD:EE:FF",
ble_pin="123456",
)
def test_tcp_and_ble_rejected(self):
with pytest.raises(ValidationError, match="Only one transport"):
Settings(
tcp_host="192.168.1.1",
ble_address="AA:BB:CC:DD:EE:FF",
ble_pin="123456",
)
def test_all_three_rejected(self):
with pytest.raises(ValidationError, match="Only one transport"):
Settings(
serial_port="/dev/ttyUSB0",
tcp_host="192.168.1.1",
ble_address="AA:BB:CC:DD:EE:FF",
ble_pin="123456",
)
class TestBLEPinRequirement:
"""BLE address requires a PIN."""
def test_ble_address_without_pin_rejected(self):
with pytest.raises(ValidationError, match="MESHCORE_BLE_PIN is required"):
Settings(ble_address="AA:BB:CC:DD:EE:FF", ble_pin="")
def test_ble_address_with_pin_accepted(self):
s = Settings(ble_address="AA:BB:CC:DD:EE:FF", ble_pin="123456")
assert s.ble_address == "AA:BB:CC:DD:EE:FF"
assert s.ble_pin == "123456"
+170
View File
@@ -0,0 +1,170 @@
"""Tests for RadioManager multi-transport connect dispatch.
These tests verify that connect() routes to the correct transport method
based on settings.connection_type, and that connection_info is set correctly.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestRadioManagerConnect:
"""Test that connect() dispatches to the correct transport."""
@pytest.mark.asyncio
async def test_connect_serial_explicit_port(self):
"""Serial connect with explicit port sets connection_info."""
from app.radio import RadioManager
mock_mc = MagicMock()
mock_mc.is_connected = True
with (
patch("app.radio.settings") as mock_settings,
patch("app.radio.MeshCore") as mock_meshcore,
):
mock_settings.connection_type = "serial"
mock_settings.serial_port = "/dev/ttyUSB0"
mock_settings.serial_baudrate = 115200
mock_meshcore.create_serial = AsyncMock(return_value=mock_mc)
rm = RadioManager()
await rm.connect()
mock_meshcore.create_serial.assert_awaited_once_with(
port="/dev/ttyUSB0",
baudrate=115200,
auto_reconnect=True,
max_reconnect_attempts=10,
)
assert rm.connection_info == "Serial: /dev/ttyUSB0"
assert rm.meshcore is mock_mc
@pytest.mark.asyncio
async def test_connect_serial_autodetect(self):
"""Serial connect without port auto-detects."""
from app.radio import RadioManager
mock_mc = MagicMock()
mock_mc.is_connected = True
with (
patch("app.radio.settings") as mock_settings,
patch("app.radio.MeshCore") as mock_meshcore,
patch("app.radio.find_radio_port", new_callable=AsyncMock) as mock_find,
):
mock_settings.connection_type = "serial"
mock_settings.serial_port = ""
mock_settings.serial_baudrate = 115200
mock_find.return_value = "/dev/ttyACM0"
mock_meshcore.create_serial = AsyncMock(return_value=mock_mc)
rm = RadioManager()
await rm.connect()
mock_find.assert_awaited_once_with(115200)
assert rm.connection_info == "Serial: /dev/ttyACM0"
@pytest.mark.asyncio
async def test_connect_serial_autodetect_fails(self):
"""Serial auto-detect raises when no radio found."""
from app.radio import RadioManager
with (
patch("app.radio.settings") as mock_settings,
patch("app.radio.find_radio_port", new_callable=AsyncMock) as mock_find,
):
mock_settings.connection_type = "serial"
mock_settings.serial_port = ""
mock_settings.serial_baudrate = 115200
mock_find.return_value = None
rm = RadioManager()
with pytest.raises(RuntimeError, match="No MeshCore radio found"):
await rm.connect()
@pytest.mark.asyncio
async def test_connect_tcp(self):
"""TCP connect sets connection_info with host:port."""
from app.radio import RadioManager
mock_mc = MagicMock()
mock_mc.is_connected = True
with (
patch("app.radio.settings") as mock_settings,
patch("app.radio.MeshCore") as mock_meshcore,
):
mock_settings.connection_type = "tcp"
mock_settings.tcp_host = "192.168.1.100"
mock_settings.tcp_port = 4000
mock_meshcore.create_tcp = AsyncMock(return_value=mock_mc)
rm = RadioManager()
await rm.connect()
mock_meshcore.create_tcp.assert_awaited_once_with(
host="192.168.1.100",
port=4000,
auto_reconnect=True,
max_reconnect_attempts=10,
)
assert rm.connection_info == "TCP: 192.168.1.100:4000"
assert rm.meshcore is mock_mc
@pytest.mark.asyncio
async def test_connect_ble(self):
"""BLE connect sets connection_info with address."""
from app.radio import RadioManager
mock_mc = MagicMock()
mock_mc.is_connected = True
with (
patch("app.radio.settings") as mock_settings,
patch("app.radio.MeshCore") as mock_meshcore,
):
mock_settings.connection_type = "ble"
mock_settings.ble_address = "AA:BB:CC:DD:EE:FF"
mock_settings.ble_pin = "123456"
mock_meshcore.create_ble = AsyncMock(return_value=mock_mc)
rm = RadioManager()
await rm.connect()
mock_meshcore.create_ble.assert_awaited_once_with(
address="AA:BB:CC:DD:EE:FF",
pin="123456",
auto_reconnect=True,
max_reconnect_attempts=15,
)
assert rm.connection_info == "BLE: AA:BB:CC:DD:EE:FF"
assert rm.meshcore is mock_mc
@pytest.mark.asyncio
async def test_connect_disconnects_existing_first(self):
"""Calling connect() when already connected disconnects first."""
from app.radio import RadioManager
old_mc = MagicMock()
old_mc.disconnect = AsyncMock()
new_mc = MagicMock()
new_mc.is_connected = True
with (
patch("app.radio.settings") as mock_settings,
patch("app.radio.MeshCore") as mock_meshcore,
):
mock_settings.connection_type = "tcp"
mock_settings.tcp_host = "10.0.0.1"
mock_settings.tcp_port = 4000
mock_meshcore.create_tcp = AsyncMock(return_value=new_mc)
rm = RadioManager()
rm._meshcore = old_mc
await rm.connect()
old_mc.disconnect.assert_awaited_once()
assert rm.meshcore is new_mc