mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-12 12:26:21 +02:00
Compare commits
10 Commits
testbranch
..
3.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
| c333eb25e3 | |||
| 580aa1cefd | |||
| 30de09f71b | |||
| 93d31adecd | |||
| 5f969017f7 | |||
| 967dd05fad | |||
| c808f0930b | |||
| 87df4b4aa1 | |||
| 0511d6f69b | |||
| 78b5598f67 |
@@ -463,7 +463,7 @@ mc.subscribe(EventType.ACK, handler)
|
|||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `MESHCORE_SERIAL_PORT` | auto-detect | Serial port for radio |
|
| `MESHCORE_SERIAL_PORT` | auto-detect | Serial port for radio |
|
||||||
| `MESHCORE_TCP_HOST` | *(none)* | TCP host for radio (mutually exclusive with serial/BLE) |
|
| `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_TCP_PORT` | `5000` | TCP port (used with `MESHCORE_TCP_HOST`) |
|
||||||
| `MESHCORE_BLE_ADDRESS` | *(none)* | BLE device address (mutually exclusive with serial/TCP) |
|
| `MESHCORE_BLE_ADDRESS` | *(none)* | BLE device address (mutually exclusive with serial/TCP) |
|
||||||
| `MESHCORE_BLE_PIN` | *(required with BLE)* | BLE PIN code |
|
| `MESHCORE_BLE_PIN` | *(required with BLE)* | BLE PIN code |
|
||||||
| `MESHCORE_SERIAL_BAUDRATE` | `115200` | Serial baud rate |
|
| `MESHCORE_SERIAL_BAUDRATE` | `115200` | Serial baud rate |
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
## [3.7.0] - 2026-04-02
|
||||||
|
|
||||||
|
* Feature: Repeater battery tracking
|
||||||
|
* Feature: Repeater info pane just like contacts
|
||||||
|
* Feature: Make repeaters blockable
|
||||||
|
* Feature: Add new-node advert blocking
|
||||||
|
* Feature: Add bulk deletion interface
|
||||||
|
* Feature: Bulk room add on alt+click of new channel button
|
||||||
|
* Feature: More info in debug endpoint
|
||||||
|
* Bugfix: Be more conservative around radio load limits and don't exceed radio-reported capacity
|
||||||
|
* Misc: Default auto-DM decrypt to true
|
||||||
|
* Misc: Reorganize some settings panes
|
||||||
|
* Misc: Enable FK pragma
|
||||||
|
* Misc: Various performance and correctness fixes
|
||||||
|
* Misc: Correct TCP default port
|
||||||
|
|
||||||
## [3.6.7] - 2026-03-31
|
## [3.6.7] - 2026-03-31
|
||||||
|
|
||||||
* Misc: Remove armv7 (for now)
|
* Misc: Remove armv7 (for now)
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ Only one transport may be active at a time. If multiple are set, the server will
|
|||||||
| `MESHCORE_SERIAL_PORT` | (auto-detect) | Serial port path |
|
| `MESHCORE_SERIAL_PORT` | (auto-detect) | Serial port path |
|
||||||
| `MESHCORE_SERIAL_BAUDRATE` | 115200 | Serial baud rate |
|
| `MESHCORE_SERIAL_BAUDRATE` | 115200 | Serial baud rate |
|
||||||
| `MESHCORE_TCP_HOST` | | TCP host (mutually exclusive with serial/BLE) |
|
| `MESHCORE_TCP_HOST` | | TCP host (mutually exclusive with serial/BLE) |
|
||||||
| `MESHCORE_TCP_PORT` | 4000 | TCP port |
|
| `MESHCORE_TCP_PORT` | 5000 | TCP port |
|
||||||
| `MESHCORE_BLE_ADDRESS` | | BLE device address (mutually exclusive with serial/TCP) |
|
| `MESHCORE_BLE_ADDRESS` | | BLE device address (mutually exclusive with serial/TCP) |
|
||||||
| `MESHCORE_BLE_PIN` | | BLE PIN (required when BLE address is set) |
|
| `MESHCORE_BLE_PIN` | | BLE PIN (required when BLE address is set) |
|
||||||
| `MESHCORE_LOG_LEVEL` | INFO | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
| `MESHCORE_LOG_LEVEL` | INFO | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
||||||
@@ -193,7 +193,7 @@ Common launch patterns:
|
|||||||
MESHCORE_SERIAL_PORT=/dev/ttyUSB0 uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
|
MESHCORE_SERIAL_PORT=/dev/ttyUSB0 uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
# TCP
|
# TCP
|
||||||
MESHCORE_TCP_HOST=192.168.1.100 MESHCORE_TCP_PORT=4000 uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
|
MESHCORE_TCP_HOST=192.168.1.100 MESHCORE_TCP_PORT=5000 uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
# BLE
|
# BLE
|
||||||
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
|
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
|
||||||
|
|||||||
+2
-1
@@ -14,7 +14,7 @@ class Settings(BaseSettings):
|
|||||||
serial_port: str = "" # Empty string triggers auto-detection
|
serial_port: str = "" # Empty string triggers auto-detection
|
||||||
serial_baudrate: int = 115200
|
serial_baudrate: int = 115200
|
||||||
tcp_host: str = ""
|
tcp_host: str = ""
|
||||||
tcp_port: int = 4000
|
tcp_port: int = 5000
|
||||||
ble_address: str = ""
|
ble_address: str = ""
|
||||||
ble_pin: str = ""
|
ble_pin: str = ""
|
||||||
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
|
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
|
||||||
@@ -26,6 +26,7 @@ class Settings(BaseSettings):
|
|||||||
default=False,
|
default=False,
|
||||||
validation_alias="__CLOWNTOWN_DO_CLOCK_WRAPAROUND",
|
validation_alias="__CLOWNTOWN_DO_CLOCK_WRAPAROUND",
|
||||||
)
|
)
|
||||||
|
skip_post_connect_sync: bool = False
|
||||||
basic_auth_username: str = ""
|
basic_auth_username: str = ""
|
||||||
basic_auth_password: str = ""
|
basic_auth_password: str = ""
|
||||||
|
|
||||||
|
|||||||
+7
-8
@@ -382,9 +382,9 @@ async def run_migrations(conn: aiosqlite.Connection) -> int:
|
|||||||
await set_version(conn, 49)
|
await set_version(conn, 49)
|
||||||
applied += 1
|
applied += 1
|
||||||
|
|
||||||
# Migration 50: Repeater telemetry history table
|
# Migration 50: Repeater telemetry history table + tracking opt-in column
|
||||||
if version < 50:
|
if version < 50:
|
||||||
logger.info("Applying migration 50: repeater telemetry history table")
|
logger.info("Applying migration 50: repeater telemetry history")
|
||||||
await _migrate_050_repeater_telemetry_history(conn)
|
await _migrate_050_repeater_telemetry_history(conn)
|
||||||
await set_version(conn, 50)
|
await set_version(conn, 50)
|
||||||
applied += 1
|
applied += 1
|
||||||
@@ -3109,10 +3109,7 @@ async def _migrate_049_foreign_key_cascade(conn: aiosqlite.Connection) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def _migrate_050_repeater_telemetry_history(conn: aiosqlite.Connection) -> None:
|
async def _migrate_050_repeater_telemetry_history(conn: aiosqlite.Connection) -> None:
|
||||||
"""Create repeater_telemetry_history table for JSON-blob telemetry snapshots.
|
"""Create repeater_telemetry_history table for JSON-blob telemetry snapshots."""
|
||||||
|
|
||||||
Uses ON DELETE CASCADE so contact deletion automatically cleans up rows.
|
|
||||||
"""
|
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS repeater_telemetry_history (
|
CREATE TABLE IF NOT EXISTS repeater_telemetry_history (
|
||||||
@@ -3125,7 +3122,9 @@ async def _migrate_050_repeater_telemetry_history(conn: aiosqlite.Connection) ->
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_repeater_telemetry_history_key_ts "
|
"""
|
||||||
"ON repeater_telemetry_history(public_key, timestamp DESC)"
|
CREATE INDEX IF NOT EXISTS idx_repeater_telemetry_pk_ts
|
||||||
|
ON repeater_telemetry_history (public_key, timestamp)
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
|
|||||||
+5
-5
@@ -535,11 +535,6 @@ class RepeaterStatusResponse(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TelemetryHistoryEntry(BaseModel):
|
|
||||||
timestamp: int
|
|
||||||
data: dict
|
|
||||||
|
|
||||||
|
|
||||||
class RepeaterNodeInfoResponse(BaseModel):
|
class RepeaterNodeInfoResponse(BaseModel):
|
||||||
"""Identity/location info from a repeater (small CLI batch)."""
|
"""Identity/location info from a repeater (small CLI batch)."""
|
||||||
|
|
||||||
@@ -929,3 +924,8 @@ class StatisticsResponse(BaseModel):
|
|||||||
known_channels_active: ContactActivityCounts
|
known_channels_active: ContactActivityCounts
|
||||||
path_hash_width_24h: PathHashWidthStats
|
path_hash_width_24h: PathHashWidthStats
|
||||||
noise_floor_24h: NoiseFloorHistoryStats
|
noise_floor_24h: NoiseFloorHistoryStats
|
||||||
|
|
||||||
|
|
||||||
|
class TelemetryHistoryEntry(BaseModel):
|
||||||
|
timestamp: int
|
||||||
|
data: dict
|
||||||
|
|||||||
@@ -204,35 +204,41 @@ async def run_post_connect_setup(radio_manager) -> None:
|
|||||||
finally:
|
finally:
|
||||||
reader.handle_rx = _original_handle_rx
|
reader.handle_rx = _original_handle_rx
|
||||||
|
|
||||||
# Sync contacts/channels from radio to DB and clear radio
|
from app.config import settings as app_settings_config
|
||||||
logger.info("Syncing and offloading radio data...")
|
|
||||||
result = await sync_and_offload_all(mc)
|
|
||||||
logger.info("Sync complete: %s", result)
|
|
||||||
|
|
||||||
# Send advertisement to announce our presence (if enabled and not throttled)
|
if app_settings_config.skip_post_connect_sync:
|
||||||
if await send_advertisement(mc):
|
logger.info("Skipping sync/offload/advert/drain (MESHCORE_SKIP_POST_CONNECT_SYNC)")
|
||||||
logger.info("Advertisement sent")
|
|
||||||
else:
|
else:
|
||||||
logger.debug("Advertisement skipped (disabled or throttled)")
|
# Sync contacts/channels from radio to DB and clear radio
|
||||||
|
logger.info("Syncing and offloading radio data...")
|
||||||
|
result = await sync_and_offload_all(mc)
|
||||||
|
logger.info("Sync complete: %s", result)
|
||||||
|
|
||||||
# Drain any messages that were queued before we connected.
|
# Send advertisement to announce our presence (if enabled and not throttled)
|
||||||
# This must happen BEFORE starting auto-fetch, otherwise both
|
if await send_advertisement(mc):
|
||||||
# compete on get_msg() with interleaved radio I/O.
|
logger.info("Advertisement sent")
|
||||||
drained = await drain_pending_messages(mc)
|
else:
|
||||||
if drained > 0:
|
logger.debug("Advertisement skipped (disabled or throttled)")
|
||||||
logger.info("Drained %d pending message(s)", drained)
|
|
||||||
radio_manager.clear_pending_message_channel_slots()
|
# Drain any messages that were queued before we connected.
|
||||||
|
# This must happen BEFORE starting auto-fetch, otherwise both
|
||||||
|
# compete on get_msg() with interleaved radio I/O.
|
||||||
|
drained = await drain_pending_messages(mc)
|
||||||
|
if drained > 0:
|
||||||
|
logger.info("Drained %d pending message(s)", drained)
|
||||||
|
radio_manager.clear_pending_message_channel_slots()
|
||||||
|
|
||||||
await mc.start_auto_message_fetching()
|
await mc.start_auto_message_fetching()
|
||||||
logger.info("Auto message fetching started")
|
logger.info("Auto message fetching started")
|
||||||
finally:
|
finally:
|
||||||
radio_manager._release_operation_lock("post_connect_setup")
|
radio_manager._release_operation_lock("post_connect_setup")
|
||||||
|
|
||||||
# Start background tasks AFTER releasing the operation lock.
|
if not app_settings_config.skip_post_connect_sync:
|
||||||
# These tasks acquire their own locks when they need radio access.
|
# Start background tasks AFTER releasing the operation lock.
|
||||||
start_periodic_sync()
|
# These tasks acquire their own locks when they need radio access.
|
||||||
start_periodic_advert()
|
start_periodic_sync()
|
||||||
start_message_polling()
|
start_periodic_advert()
|
||||||
|
start_message_polling()
|
||||||
|
|
||||||
radio_manager._setup_complete = True
|
radio_manager._setup_complete = True
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "remoteterm-meshcore-frontend",
|
"name": "remoteterm-meshcore-frontend",
|
||||||
"version": "3.6.2",
|
"version": "3.6.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "remoteterm-meshcore-frontend",
|
"name": "remoteterm-meshcore-frontend",
|
||||||
"version": "3.6.2",
|
"version": "3.6.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-python": "^6.2.1",
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "remoteterm-meshcore-frontend",
|
"name": "remoteterm-meshcore-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "3.6.7",
|
"version": "3.7.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import { toast } from './ui/sonner';
|
import { toast } from './ui/sonner';
|
||||||
@@ -100,23 +100,38 @@ export function RepeaterDashboard({
|
|||||||
|
|
||||||
// Telemetry history: preload from stored data, refresh from live status
|
// Telemetry history: preload from stored data, refresh from live status
|
||||||
const [telemetryHistory, setTelemetryHistory] = useState<TelemetryHistoryEntry[]>([]);
|
const [telemetryHistory, setTelemetryHistory] = useState<TelemetryHistoryEntry[]>([]);
|
||||||
|
const telemetryHistorySourceRef = useRef<'none' | 'preload' | 'live'>('none');
|
||||||
|
const telemetryHistoryRequestRef = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
telemetryHistoryRequestRef.current += 1;
|
||||||
|
telemetryHistorySourceRef.current = 'none';
|
||||||
|
setTelemetryHistory([]);
|
||||||
|
|
||||||
if (!loggedIn) return;
|
if (!loggedIn) return;
|
||||||
|
|
||||||
|
const requestId = telemetryHistoryRequestRef.current;
|
||||||
api
|
api
|
||||||
.repeaterTelemetryHistory(conversation.id)
|
.repeaterTelemetryHistory(conversation.id)
|
||||||
.then(setTelemetryHistory)
|
.then((history) => {
|
||||||
|
if (telemetryHistoryRequestRef.current !== requestId) return;
|
||||||
|
if (telemetryHistorySourceRef.current === 'live') return;
|
||||||
|
telemetryHistorySourceRef.current = 'preload';
|
||||||
|
setTelemetryHistory(history);
|
||||||
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [loggedIn, conversation.id]);
|
}, [loggedIn, conversation.id]);
|
||||||
|
|
||||||
// When a live status fetch returns embedded telemetry_history, replace local state
|
// When a live status fetch returns embedded telemetry_history, replace local state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const liveHistory = paneData.status?.telemetry_history;
|
const liveHistory = paneData.status?.telemetry_history;
|
||||||
if (liveHistory && liveHistory.length > 0) {
|
if (!liveHistory) return;
|
||||||
setTelemetryHistory(liveHistory);
|
telemetryHistorySourceRef.current = 'live';
|
||||||
}
|
setTelemetryHistory(liveHistory);
|
||||||
}, [paneData.status?.telemetry_history]);
|
}, [paneData.status?.telemetry_history]);
|
||||||
|
|
||||||
const isFav = isFavorite(favorites, 'contact', conversation.id);
|
const isFav = isFavorite(favorites, 'contact', conversation.id);
|
||||||
|
|
||||||
const handleRepeaterLogin = async (nextPassword: string) => {
|
const handleRepeaterLogin = async (nextPassword: string) => {
|
||||||
await login(nextPassword);
|
await login(nextPassword);
|
||||||
persistAfterLogin(nextPassword);
|
persistAfterLogin(nextPassword);
|
||||||
@@ -317,7 +332,6 @@ export function RepeaterDashboard({
|
|||||||
onRefresh={() => refreshPane('status')}
|
onRefresh={() => refreshPane('status')}
|
||||||
disabled={anyLoading}
|
disabled={anyLoading}
|
||||||
/>
|
/>
|
||||||
<TelemetryHistoryPane entries={telemetryHistory} />
|
|
||||||
<RadioSettingsPane
|
<RadioSettingsPane
|
||||||
data={paneData.radioSettings}
|
data={paneData.radioSettings}
|
||||||
state={paneStates.radioSettings}
|
state={paneStates.radioSettings}
|
||||||
@@ -380,6 +394,9 @@ export function RepeaterDashboard({
|
|||||||
loading={consoleLoading}
|
loading={consoleLoading}
|
||||||
onSend={sendConsoleCommand}
|
onSend={sendConsoleCommand}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Telemetry history chart — full width, below console */}
|
||||||
|
<TelemetryHistoryPane entries={telemetryHistory} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -126,6 +126,16 @@ const defaultProps = {
|
|||||||
onDeleteContact: vi.fn(),
|
onDeleteContact: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function createDeferred<T>() {
|
||||||
|
let resolve!: (value: T) => void;
|
||||||
|
let reject!: (reason?: unknown) => void;
|
||||||
|
const promise = new Promise<T>((res, rej) => {
|
||||||
|
resolve = res;
|
||||||
|
reject = rej;
|
||||||
|
});
|
||||||
|
return { promise, resolve, reject };
|
||||||
|
}
|
||||||
|
|
||||||
describe('RepeaterDashboard', () => {
|
describe('RepeaterDashboard', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -645,6 +655,11 @@ describe('RepeaterDashboard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('telemetry history', () => {
|
describe('telemetry history', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const { api } = await import('../api');
|
||||||
|
vi.mocked(api.repeaterTelemetryHistory).mockResolvedValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('loads telemetry history on mount when logged in', async () => {
|
it('loads telemetry history on mount when logged in', async () => {
|
||||||
const { api } = await import('../api');
|
const { api } = await import('../api');
|
||||||
mockHook.loggedIn = true;
|
mockHook.loggedIn = true;
|
||||||
@@ -699,5 +714,45 @@ describe('RepeaterDashboard', () => {
|
|||||||
expect(screen.getByText('1 samples')).toBeInTheDocument();
|
expect(screen.getByText('1 samples')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not let an older preload overwrite newer live status history', async () => {
|
||||||
|
const { api } = await import('../api');
|
||||||
|
const historySpy = vi.mocked(api.repeaterTelemetryHistory);
|
||||||
|
const deferred = createDeferred<{ timestamp: number; data: { battery_volts: number } }[]>();
|
||||||
|
historySpy.mockReturnValue(deferred.promise);
|
||||||
|
|
||||||
|
mockHook.loggedIn = true;
|
||||||
|
mockHook.paneData.status = {
|
||||||
|
battery_volts: 4.2,
|
||||||
|
tx_queue_len: 0,
|
||||||
|
noise_floor_dbm: -120,
|
||||||
|
last_rssi_dbm: -85,
|
||||||
|
last_snr_db: 7.5,
|
||||||
|
packets_received: 100,
|
||||||
|
packets_sent: 50,
|
||||||
|
airtime_seconds: 600,
|
||||||
|
rx_airtime_seconds: 1200,
|
||||||
|
uptime_seconds: 86400,
|
||||||
|
sent_flood: 10,
|
||||||
|
sent_direct: 40,
|
||||||
|
recv_flood: 30,
|
||||||
|
recv_direct: 70,
|
||||||
|
flood_dups: 1,
|
||||||
|
direct_dups: 0,
|
||||||
|
full_events: 0,
|
||||||
|
telemetry_history: [{ timestamp: 1700000000, data: { battery_volts: 4.2 } }],
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<RepeaterDashboard {...defaultProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('1 samples')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
deferred.resolve([{ timestamp: 1690000000, data: { battery_volts: 3.9 } }]);
|
||||||
|
await deferred.promise;
|
||||||
|
|
||||||
|
expect(screen.getByText('1 samples')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,3 +7,19 @@ class ResizeObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalThis.ResizeObserver = ResizeObserver;
|
globalThis.ResizeObserver = ResizeObserver;
|
||||||
|
|
||||||
|
// Several components call matchMedia at import time for responsive detection
|
||||||
|
if (typeof globalThis.matchMedia === 'undefined') {
|
||||||
|
Object.defineProperty(globalThis, 'matchMedia', {
|
||||||
|
value: (query: string) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: () => {},
|
||||||
|
removeListener: () => {},
|
||||||
|
addEventListener: () => {},
|
||||||
|
removeEventListener: () => {},
|
||||||
|
dispatchEvent: () => false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+13
-13
@@ -235,14 +235,6 @@ export interface ChannelTopSender {
|
|||||||
message_count: number;
|
message_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChannelDetail {
|
|
||||||
channel: Channel;
|
|
||||||
message_counts: ChannelMessageCounts;
|
|
||||||
first_message_at: number | null;
|
|
||||||
unique_sender_count: number;
|
|
||||||
top_senders_24h: ChannelTopSender[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BulkCreateHashtagChannelsResult {
|
export interface BulkCreateHashtagChannelsResult {
|
||||||
created_channels: Channel[];
|
created_channels: Channel[];
|
||||||
existing_count: number;
|
existing_count: number;
|
||||||
@@ -252,6 +244,14 @@ export interface BulkCreateHashtagChannelsResult {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChannelDetail {
|
||||||
|
channel: Channel;
|
||||||
|
message_counts: ChannelMessageCounts;
|
||||||
|
first_message_at: number | null;
|
||||||
|
unique_sender_count: number;
|
||||||
|
top_senders_24h: ChannelTopSender[];
|
||||||
|
}
|
||||||
|
|
||||||
/** A single path that a message took to reach us */
|
/** A single path that a message took to reach us */
|
||||||
export interface MessagePath {
|
export interface MessagePath {
|
||||||
/** Hex-encoded routing path */
|
/** Hex-encoded routing path */
|
||||||
@@ -419,11 +419,6 @@ export interface RepeaterStatusResponse {
|
|||||||
telemetry_history: TelemetryHistoryEntry[];
|
telemetry_history: TelemetryHistoryEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TelemetryHistoryEntry {
|
|
||||||
timestamp: number;
|
|
||||||
data: Record<string, number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RepeaterNeighborsResponse {
|
export interface RepeaterNeighborsResponse {
|
||||||
neighbors: NeighborInfo[];
|
neighbors: NeighborInfo[];
|
||||||
}
|
}
|
||||||
@@ -485,6 +480,11 @@ export interface PaneState {
|
|||||||
fetched_at?: number | null;
|
fetched_at?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TelemetryHistoryEntry {
|
||||||
|
timestamp: number;
|
||||||
|
data: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TraceResponse {
|
export interface TraceResponse {
|
||||||
remote_snr: number | null;
|
remote_snr: number | null;
|
||||||
local_snr: number | null;
|
local_snr: number | null;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "remoteterm-meshcore"
|
name = "remoteterm-meshcore"
|
||||||
version = "3.6.7"
|
version = "3.7.0"
|
||||||
description = "RemoteTerm - Web interface for MeshCore radio mesh networks"
|
description = "RemoteTerm - Web interface for MeshCore radio mesh networks"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export default defineConfig({
|
|||||||
timeout: 180_000,
|
timeout: 180_000,
|
||||||
env: {
|
env: {
|
||||||
MESHCORE_DATABASE_PATH: path.join(tmpDir, 'e2e-test.db'),
|
MESHCORE_DATABASE_PATH: path.join(tmpDir, 'e2e-test.db'),
|
||||||
|
MESHCORE_SKIP_POST_CONNECT_SYNC: 'true',
|
||||||
// Pass through the serial port from the environment
|
// Pass through the serial port from the environment
|
||||||
...(process.env.MESHCORE_SERIAL_PORT
|
...(process.env.MESHCORE_SERIAL_PORT
|
||||||
? { MESHCORE_SERIAL_PORT: process.env.MESHCORE_SERIAL_PORT }
|
? { MESHCORE_SERIAL_PORT: process.env.MESHCORE_SERIAL_PORT }
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class TestTransportExclusivity:
|
|||||||
|
|
||||||
def test_tcp_default_port(self):
|
def test_tcp_default_port(self):
|
||||||
s = Settings(tcp_host="192.168.1.1")
|
s = Settings(tcp_host="192.168.1.1")
|
||||||
assert s.tcp_port == 4000
|
assert s.tcp_port == 5000
|
||||||
|
|
||||||
def test_ble_only(self):
|
def test_ble_only(self):
|
||||||
s = Settings(ble_address="AA:BB:CC:DD:EE:FF", ble_pin="123456")
|
s = Settings(ble_address="AA:BB:CC:DD:EE:FF", ble_pin="123456")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Tests for repeater telemetry history: repository CRUD and read-only endpoint."""
|
"""Tests for repeater telemetry history: repository CRUD and embedded status response."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|||||||
@@ -1098,7 +1098,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remoteterm-meshcore"
|
name = "remoteterm-meshcore"
|
||||||
version = "3.6.7"
|
version = "3.7.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiomqtt" },
|
{ name = "aiomqtt" },
|
||||||
|
|||||||
Reference in New Issue
Block a user