2026-06-16 03:40:31 +00:00
|
|
|
// API types matching actual backend responses
|
|
|
|
|
|
|
|
|
|
export interface SystemStatus {
|
|
|
|
|
version: string
|
|
|
|
|
uptime_seconds: number
|
|
|
|
|
bot_name: string
|
|
|
|
|
connection_type: string
|
|
|
|
|
connection_target: string
|
|
|
|
|
connected: boolean
|
|
|
|
|
node_count: number
|
|
|
|
|
source_count: number
|
|
|
|
|
env_feeds_enabled: boolean
|
|
|
|
|
dashboard_port: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface MeshHealth {
|
|
|
|
|
score: number
|
|
|
|
|
tier: string
|
|
|
|
|
pillars: {
|
|
|
|
|
infrastructure: number
|
|
|
|
|
utilization: number
|
|
|
|
|
coverage: number
|
|
|
|
|
behavior: number
|
|
|
|
|
power: number
|
|
|
|
|
}
|
|
|
|
|
infra_online: number
|
|
|
|
|
infra_total: number
|
|
|
|
|
util_percent: number
|
|
|
|
|
flagged_nodes: number
|
|
|
|
|
battery_warnings: number
|
|
|
|
|
total_nodes: number
|
|
|
|
|
total_regions: number
|
|
|
|
|
unlocated_count: number
|
|
|
|
|
last_computed: string
|
|
|
|
|
recommendations: string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface NodeInfo {
|
|
|
|
|
node_num: number
|
|
|
|
|
node_id_hex: string
|
|
|
|
|
short_name: string
|
|
|
|
|
long_name: string
|
|
|
|
|
role: string
|
|
|
|
|
latitude: number | null
|
|
|
|
|
longitude: number | null
|
|
|
|
|
last_heard: string | null
|
|
|
|
|
battery_level: number | null
|
|
|
|
|
voltage: number | null
|
|
|
|
|
snr: number | null
|
|
|
|
|
firmware: string
|
|
|
|
|
hardware: string
|
|
|
|
|
uptime: number | null
|
|
|
|
|
sources: string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface EdgeInfo {
|
|
|
|
|
from_node: number
|
|
|
|
|
to_node: number
|
|
|
|
|
snr: number
|
|
|
|
|
quality: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RegionInfo {
|
|
|
|
|
name: string
|
|
|
|
|
local_name: string
|
|
|
|
|
node_count: number
|
|
|
|
|
infra_count: number
|
|
|
|
|
infra_online: number
|
|
|
|
|
online_count: number
|
|
|
|
|
score: number
|
|
|
|
|
tier: string
|
|
|
|
|
center_lat: number
|
|
|
|
|
center_lon: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SourceHealth {
|
|
|
|
|
name: string
|
|
|
|
|
type: string
|
|
|
|
|
url: string
|
|
|
|
|
is_loaded: boolean
|
|
|
|
|
last_error: string | null
|
|
|
|
|
consecutive_errors: number
|
|
|
|
|
response_time_ms: number | null
|
|
|
|
|
tick_count: number
|
|
|
|
|
node_count: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Alert {
|
|
|
|
|
type: string
|
|
|
|
|
severity: string
|
|
|
|
|
message: string
|
|
|
|
|
timestamp: string
|
|
|
|
|
scope_type?: string
|
|
|
|
|
scope_value?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AlertHistoryItem {
|
|
|
|
|
id?: number
|
|
|
|
|
type: string
|
|
|
|
|
severity: string
|
|
|
|
|
message: string
|
|
|
|
|
timestamp: string
|
|
|
|
|
duration?: number
|
|
|
|
|
scope_type?: string
|
|
|
|
|
scope_value?: string
|
|
|
|
|
resolved_at?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AlertHistoryResponse {
|
|
|
|
|
items: AlertHistoryItem[]
|
|
|
|
|
total: number
|
|
|
|
|
}
|
|
|
|
|
|
Phase A — 4-section nav; move Scheduled Broadcasts + Danger Zones off Routing (#15)
* feat(dashboard): Phase A — 4-section nav; move Scheduled Broadcasts + Danger Zones off Routing
Regroup nav into GENERAL/MESHTASTIC/MESHCORE/DOCUMENTATION (<=5 pages each,
MT & MC mirror). Consolidate via tabs (Places, Nodes & Health, Contacts &
Companion) reusing existing components. Move Band Conditions, cold-start,
and fire digest to per-mesh Scheduled Broadcasts pages; move Danger Zones
to its own page. Routing keeps its sending rules unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase B — clean identical Routing grids; relocate per-family gating to Data Feeds
Meshtastic Routing becomes an always-visible pure-delivery grid matching
MeshCore (no master-toggle expand/collapse). Per-family gating (enable/
severity/freshness/cooldown) moves to a Family Settings section on Data
Feeds. Sending rules + Notification Rules unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase C — MeshCore bot-behavior parity (observe channels / ignore contacts / DMs)
Add meshcore context (observe channels by name, ignore contacts, DM policy)
and wire the MeshCore inbound path to honor it, mirroring Meshtastic's
observe/ignore filtering. Symmetric "Bot behavior" sections on both
Connection pages. Meshtastic path unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase D — dedupe Environment/Adapter Config into one Data Feeds surface
Curated family panels are the single home for the shared adapter keys;
Adapter Config becomes an Advanced/raw escape hatch (owned keys no longer
double-editable). Surface include_in_llm_context per adapter. Fix the
adapter-config array-vs-object parsing (fire digest values now load).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase E — Activity Log (per-mesh broadcast feed); remove subscription backend
Replace Alerts with an Activity Log fed by per-mesh broadcast logging
(transport+channel+success on mesh_broadcasts_out, additive migration).
Remove the entire subscription backend (commands, DM dispatch, storage,
API) and its UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:57:24 -06:00
|
|
|
export interface ActivityEntry {
|
2026-06-16 03:40:31 +00:00
|
|
|
id: number
|
Phase A — 4-section nav; move Scheduled Broadcasts + Danger Zones off Routing (#15)
* feat(dashboard): Phase A — 4-section nav; move Scheduled Broadcasts + Danger Zones off Routing
Regroup nav into GENERAL/MESHTASTIC/MESHCORE/DOCUMENTATION (<=5 pages each,
MT & MC mirror). Consolidate via tabs (Places, Nodes & Health, Contacts &
Companion) reusing existing components. Move Band Conditions, cold-start,
and fire digest to per-mesh Scheduled Broadcasts pages; move Danger Zones
to its own page. Routing keeps its sending rules unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase B — clean identical Routing grids; relocate per-family gating to Data Feeds
Meshtastic Routing becomes an always-visible pure-delivery grid matching
MeshCore (no master-toggle expand/collapse). Per-family gating (enable/
severity/freshness/cooldown) moves to a Family Settings section on Data
Feeds. Sending rules + Notification Rules unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase C — MeshCore bot-behavior parity (observe channels / ignore contacts / DMs)
Add meshcore context (observe channels by name, ignore contacts, DM policy)
and wire the MeshCore inbound path to honor it, mirroring Meshtastic's
observe/ignore filtering. Symmetric "Bot behavior" sections on both
Connection pages. Meshtastic path unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase D — dedupe Environment/Adapter Config into one Data Feeds surface
Curated family panels are the single home for the shared adapter keys;
Adapter Config becomes an Advanced/raw escape hatch (owned keys no longer
double-editable). Surface include_in_llm_context per adapter. Fix the
adapter-config array-vs-object parsing (fire digest values now load).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dashboard): Phase E — Activity Log (per-mesh broadcast feed); remove subscription backend
Replace Alerts with an Activity Log fed by per-mesh broadcast logging
(transport+channel+success on mesh_broadcasts_out, additive migration).
Remove the entire subscription backend (commands, DM dispatch, storage,
API) and its UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:57:24 -06:00
|
|
|
sent_at: number | string | null // epoch seconds (int) on new rows
|
|
|
|
|
recipient: string | null
|
|
|
|
|
channel: string | number | null
|
|
|
|
|
text: string | null
|
|
|
|
|
source_event_table: string | null
|
|
|
|
|
source_event_pk: string | number | null
|
|
|
|
|
bytes_sent: number | null
|
|
|
|
|
ack_received: number | null
|
|
|
|
|
transport: string | null // 'meshtastic' | 'meshcore' | null (legacy)
|
|
|
|
|
success: number | null // 1 sent, 0 skip/fail, null legacy
|
2026-06-16 03:40:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface EnvStatus {
|
|
|
|
|
enabled: boolean
|
|
|
|
|
feeds: EnvFeedHealth[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface EnvFeedHealth {
|
|
|
|
|
source: string
|
|
|
|
|
is_loaded: boolean
|
|
|
|
|
last_error: string | null
|
|
|
|
|
consecutive_errors: number
|
|
|
|
|
event_count: number
|
|
|
|
|
last_fetch: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface EnvEvent {
|
|
|
|
|
source: string
|
|
|
|
|
event_id: string
|
|
|
|
|
event_type: string
|
|
|
|
|
severity: string
|
|
|
|
|
headline: string
|
|
|
|
|
description?: string
|
|
|
|
|
expires?: number
|
|
|
|
|
fetched_at: number
|
|
|
|
|
[key: string]: unknown
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Kp history entry for charting
|
|
|
|
|
export interface KpHistoryEntry {
|
|
|
|
|
time: string
|
|
|
|
|
value: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SFI history entry for charting
|
|
|
|
|
export interface SfiHistoryEntry {
|
|
|
|
|
time: string
|
|
|
|
|
value: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refractivity profile entry
|
|
|
|
|
export interface ProfileEntry {
|
|
|
|
|
level_hPa: number
|
|
|
|
|
height_m: number
|
|
|
|
|
N: number
|
|
|
|
|
M: number
|
|
|
|
|
T_C: number
|
|
|
|
|
RH: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Gradient entry
|
|
|
|
|
export interface GradientEntry {
|
|
|
|
|
from_level: number
|
|
|
|
|
to_level: number
|
|
|
|
|
from_height_m: number
|
|
|
|
|
to_height_m: number
|
|
|
|
|
gradient: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface BandConditionsStatus {
|
|
|
|
|
enabled: boolean
|
|
|
|
|
ratings?: {
|
|
|
|
|
"80-40m"?: string
|
|
|
|
|
"30-20m"?: string
|
|
|
|
|
"17-15m"?: string
|
|
|
|
|
"12-10m"?: string
|
|
|
|
|
}
|
|
|
|
|
slot_label?: string
|
|
|
|
|
sent_at?: number
|
|
|
|
|
source?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Kept for backward compat references
|
|
|
|
|
export type SWPCStatus = BandConditionsStatus
|
|
|
|
|
|
|
|
|
|
export interface DuctingStatus {
|
|
|
|
|
enabled: boolean
|
|
|
|
|
condition?: string
|
|
|
|
|
min_gradient?: number
|
|
|
|
|
duct_thickness_m?: number | null
|
|
|
|
|
duct_base_m?: number | null
|
|
|
|
|
last_update?: string
|
|
|
|
|
profile?: ProfileEntry[]
|
|
|
|
|
gradients?: GradientEntry[]
|
|
|
|
|
assessment?: string
|
|
|
|
|
location?: { lat: number; lon: number }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RFPropagation {
|
|
|
|
|
hf: {
|
|
|
|
|
kp_current?: number
|
|
|
|
|
sfi?: number
|
|
|
|
|
r_scale?: number
|
|
|
|
|
s_scale?: number
|
|
|
|
|
g_scale?: number
|
|
|
|
|
active_warnings?: string[]
|
|
|
|
|
kp_history?: KpHistoryEntry[]
|
|
|
|
|
}
|
|
|
|
|
uhf_ducting: {
|
|
|
|
|
condition?: string
|
|
|
|
|
min_gradient?: number
|
|
|
|
|
duct_thickness_m?: number | null
|
|
|
|
|
profile?: ProfileEntry[]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// API fetch helpers
|
|
|
|
|
|
|
|
|
|
async function fetchJson<T>(url: string): Promise<T> {
|
|
|
|
|
const response = await fetch(url)
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`API error: ${response.status} ${response.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return response.json()
|
|
|
|
|
}
|
|
|
|
|
|
feat(transport): MeshCore serial/USB + BLE support + USB auto-detect with stable paths (#52)
MeshCore can now connect over USB serial (and BLE) directly, not just TCP to
the pyMC companion. The meshcore lib already supported create_serial/create_ble;
we just wire it up. Plus a USB auto-detect scanner that resolves stable device
paths to fix ttyACM enumeration hopping across replug/reboot.
Backend:
- ConnectionConfig: meshcore_conn_type (tcp|serial|ble, default tcp),
meshcore_serial_port, meshcore_baud=115200, meshcore_ble_address (validated)
- meshcore_transport._do_connect dispatches per mode: serial ->
MeshCore.create_serial(port, baudrate, auto_reconnect, max_reconnect_attempts),
ble -> create_ble(address or None), tcp -> create_tcp (unchanged). Mode-aware
logging/reconnect. Transport otherwise unchanged (mode-agnostic once _mc exists).
- factory.meshcore_enabled(config): active when the selected mode is configured
(serial port / ble address / tcp host); back-compat — meshcore_host + default
tcp still activates exactly as before.
- serial_ports.list_serial_ports(): pyserial comports + stable_path resolution
by-id -> by-path -> raw (by-id keyed on USB serial = stable across replug),
likely_radio flag by VID (RAK/nRF/CP210x/CH340), excludes legacy ttyS*, never
raises. GET /api/serial-ports (+ container by-id passthrough hint).
Frontend:
- SerialPortPicker component: "Detect USB devices" -> lists ports (likely-radio
badge, shows stable_path) -> onChange sets the stable by-id path; manual text
fallback; empty/error/note states.
- MeshCore Connection: type selector TCP/Serial/BLE + per-mode fields (serial
picker + baud; ble address). Meshtastic serial branch now uses the picker too.
Code-ready; not activated (defaults keep TCP). 35 new tests; suite at 10-failure
baseline. Container needs /dev/serial passed through for by-id paths.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 22:12:03 -06:00
|
|
|
export interface SerialPort {
|
|
|
|
|
device: string
|
|
|
|
|
by_id?: string | null
|
|
|
|
|
by_path?: string | null
|
|
|
|
|
stable_path: string
|
|
|
|
|
description?: string
|
|
|
|
|
vid?: number | null
|
|
|
|
|
pid?: number | null
|
|
|
|
|
serial_number?: string | null
|
|
|
|
|
manufacturer?: string | null
|
|
|
|
|
product?: string | null
|
|
|
|
|
likely_radio: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SerialPortsResponse {
|
|
|
|
|
ports: SerialPort[]
|
|
|
|
|
note: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getSerialPorts(): Promise<SerialPortsResponse> {
|
|
|
|
|
return fetchJson<SerialPortsResponse>('/api/serial-ports')
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 03:40:31 +00:00
|
|
|
export async function fetchStatus(): Promise<SystemStatus> {
|
|
|
|
|
return fetchJson<SystemStatus>('/api/status')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchHealth(): Promise<MeshHealth> {
|
|
|
|
|
return fetchJson<MeshHealth>('/api/health')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchNodes(): Promise<NodeInfo[]> {
|
|
|
|
|
return fetchJson<NodeInfo[]>('/api/nodes')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchEdges(): Promise<EdgeInfo[]> {
|
|
|
|
|
return fetchJson<EdgeInfo[]>('/api/edges')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchSources(): Promise<SourceHealth[]> {
|
|
|
|
|
return fetchJson<SourceHealth[]>('/api/sources')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchConfig(section?: string): Promise<unknown> {
|
|
|
|
|
const url = section ? `/api/config/${section}` : '/api/config'
|
|
|
|
|
return fetchJson(url)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function updateConfig(
|
|
|
|
|
section: string,
|
|
|
|
|
data: unknown
|
|
|
|
|
): Promise<{ saved: boolean; restart_required: boolean }> {
|
|
|
|
|
const response = await fetch(`/api/config/${section}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
})
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`API error: ${response.status} ${response.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return response.json()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 00:49:42 -06:00
|
|
|
// ---- Generic (no-code) data sources ------------------------------------
|
|
|
|
|
|
|
|
|
|
export interface FieldMapping {
|
|
|
|
|
source_path: string
|
|
|
|
|
dest_key: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GenericSource {
|
|
|
|
|
name: string
|
|
|
|
|
enabled: boolean
|
|
|
|
|
url: string
|
|
|
|
|
items_path: string
|
|
|
|
|
id_path: string
|
|
|
|
|
lat_path?: string
|
|
|
|
|
lon_path?: string
|
|
|
|
|
geometry_path?: string
|
|
|
|
|
title_path?: string
|
|
|
|
|
time_path?: string
|
|
|
|
|
category: string
|
|
|
|
|
poll_seconds: number
|
|
|
|
|
severity: string
|
|
|
|
|
field_mappings: FieldMapping[]
|
|
|
|
|
summary_template?: string
|
|
|
|
|
emoji?: string
|
2026-07-07 02:24:27 -06:00
|
|
|
// Optional custom request headers (e.g. User-Agent override or Authorization).
|
|
|
|
|
// Empty/absent = the default browser UA. Sent on both poll and preview.
|
|
|
|
|
headers?: Record<string, string>
|
2026-07-07 00:49:42 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GenericSourcePreview {
|
|
|
|
|
ok: boolean
|
|
|
|
|
status?: number
|
|
|
|
|
error?: string
|
|
|
|
|
sample?: string
|
|
|
|
|
item_count?: number | null
|
|
|
|
|
first_item?: string
|
|
|
|
|
items_path_note?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchGenericSources(): Promise<GenericSource[]> {
|
|
|
|
|
return fetchJson<GenericSource[]>('/api/config/generic_sources')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function saveGenericSources(
|
|
|
|
|
sources: GenericSource[]
|
|
|
|
|
): Promise<{ saved: boolean; restart_required: boolean; changed_keys?: string[] }> {
|
|
|
|
|
const response = await fetch('/api/config/generic_sources', {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(sources),
|
|
|
|
|
})
|
|
|
|
|
const result = await response.json()
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error((result as { detail?: string }).detail || `Save failed (${response.status})`)
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function previewGenericSource(
|
|
|
|
|
url: string,
|
2026-07-07 02:24:27 -06:00
|
|
|
items_path?: string,
|
|
|
|
|
headers?: Record<string, string>
|
2026-07-07 00:49:42 -06:00
|
|
|
): Promise<GenericSourcePreview> {
|
|
|
|
|
const response = await fetch('/api/generic-sources/preview', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
2026-07-07 02:24:27 -06:00
|
|
|
body: JSON.stringify({ url, items_path, headers }),
|
2026-07-07 00:49:42 -06:00
|
|
|
})
|
|
|
|
|
// The endpoint never raises; it returns {ok:false,error} on failure too.
|
|
|
|
|
return response.json()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 03:40:31 +00:00
|
|
|
export async function fetchAlerts(): Promise<Alert[]> {
|
|
|
|
|
return fetchJson<Alert[]>('/api/alerts/active')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchAlertHistory(
|
|
|
|
|
limit: number = 50,
|
|
|
|
|
offset: number = 0,
|
|
|
|
|
type?: string,
|
|
|
|
|
severity?: string
|
|
|
|
|
): Promise<AlertHistoryResponse | AlertHistoryItem[]> {
|
|
|
|
|
const params = new URLSearchParams()
|
|
|
|
|
params.set('limit', limit.toString())
|
|
|
|
|
params.set('offset', offset.toString())
|
|
|
|
|
if (type && type !== 'all') params.set('type', type)
|
|
|
|
|
if (severity && severity !== 'all') params.set('severity', severity)
|
|
|
|
|
return fetchJson<AlertHistoryResponse | AlertHistoryItem[]>(`/api/alerts/history?${params.toString()}`)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 19:36:30 -06:00
|
|
|
export async function fetchActivity(
|
|
|
|
|
limit = 100,
|
|
|
|
|
transport?: string,
|
|
|
|
|
category?: string,
|
|
|
|
|
): Promise<ActivityEntry[]> {
|
|
|
|
|
const params = new URLSearchParams()
|
|
|
|
|
params.set('limit', limit.toString())
|
|
|
|
|
if (transport && transport !== 'all') params.set('transport', transport)
|
|
|
|
|
if (category && category !== 'all') params.set('category', category)
|
|
|
|
|
return fetchJson<ActivityEntry[]>(`/api/activity?${params.toString()}`)
|
2026-06-16 03:40:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchEnvStatus(): Promise<EnvStatus> {
|
|
|
|
|
return fetchJson<EnvStatus>('/api/env/status')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchEnvActive(): Promise<EnvEvent[]> {
|
|
|
|
|
return fetchJson<EnvEvent[]>('/api/env/active')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchRFPropagation(): Promise<RFPropagation> {
|
|
|
|
|
return fetchJson<RFPropagation>('/api/env/propagation')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchSWPC(): Promise<BandConditionsStatus> {
|
|
|
|
|
return fetchJson<BandConditionsStatus>('/api/env/swpc')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchDucting(): Promise<DuctingStatus> {
|
|
|
|
|
return fetchJson<DuctingStatus>('/api/env/ducting')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface FireEvent {
|
|
|
|
|
source: string
|
|
|
|
|
event_id: string
|
|
|
|
|
event_type: string
|
|
|
|
|
severity: string
|
|
|
|
|
headline: string
|
|
|
|
|
name: string
|
|
|
|
|
acres: number
|
|
|
|
|
pct_contained: number
|
|
|
|
|
lat: number | null
|
|
|
|
|
lon: number | null
|
|
|
|
|
distance_km: number | null
|
|
|
|
|
nearest_anchor: string | null
|
|
|
|
|
state: string
|
|
|
|
|
expires: number
|
|
|
|
|
fetched_at: number
|
|
|
|
|
polygon?: number[][][]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AvalancheEvent {
|
|
|
|
|
source: string
|
|
|
|
|
event_id: string
|
|
|
|
|
event_type: string
|
|
|
|
|
severity: string
|
|
|
|
|
headline: string
|
|
|
|
|
zone_name: string
|
|
|
|
|
center: string
|
|
|
|
|
center_id: string
|
|
|
|
|
center_link: string
|
|
|
|
|
forecast_link: string
|
|
|
|
|
danger: string
|
|
|
|
|
danger_level: number
|
|
|
|
|
danger_name: string
|
|
|
|
|
travel_advice: string
|
|
|
|
|
state: string
|
|
|
|
|
lat: number | null
|
|
|
|
|
lon: number | null
|
|
|
|
|
expires: number
|
|
|
|
|
fetched_at: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface StreamGaugeEvent {
|
|
|
|
|
source: string
|
|
|
|
|
event_id: string
|
|
|
|
|
event_type: string
|
|
|
|
|
headline: string
|
|
|
|
|
severity: string
|
|
|
|
|
lat?: number
|
|
|
|
|
lon?: number
|
|
|
|
|
expires: number
|
|
|
|
|
fetched_at: number
|
|
|
|
|
properties: {
|
|
|
|
|
site_id: string
|
|
|
|
|
site_name: string
|
|
|
|
|
parameter: string
|
|
|
|
|
value: number
|
|
|
|
|
unit: string
|
|
|
|
|
timestamp: string
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface TrafficEvent {
|
|
|
|
|
source: string
|
|
|
|
|
event_id: string
|
|
|
|
|
event_type: string
|
|
|
|
|
headline: string
|
|
|
|
|
severity: string
|
|
|
|
|
lat?: number
|
|
|
|
|
lon?: number
|
|
|
|
|
expires: number
|
|
|
|
|
fetched_at: number
|
|
|
|
|
properties: {
|
|
|
|
|
corridor: string
|
|
|
|
|
currentSpeed: number
|
|
|
|
|
freeFlowSpeed: number
|
|
|
|
|
speedRatio: number
|
|
|
|
|
currentTravelTime: number
|
|
|
|
|
freeFlowTravelTime: number
|
|
|
|
|
confidence: number
|
|
|
|
|
roadClosure: boolean
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RoadEvent {
|
|
|
|
|
source: string
|
|
|
|
|
event_id: string
|
|
|
|
|
event_type: string
|
|
|
|
|
headline: string
|
|
|
|
|
description?: string
|
|
|
|
|
severity: string
|
|
|
|
|
lat?: number
|
|
|
|
|
lon?: number
|
|
|
|
|
expires: number
|
|
|
|
|
fetched_at: number
|
|
|
|
|
properties: {
|
|
|
|
|
roadway: string
|
|
|
|
|
is_closure: boolean
|
|
|
|
|
last_updated?: string
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface HotspotEvent {
|
|
|
|
|
source: string
|
|
|
|
|
event_id: string
|
|
|
|
|
event_type: string
|
|
|
|
|
headline: string
|
|
|
|
|
severity: string
|
|
|
|
|
lat?: number
|
|
|
|
|
lon?: number
|
|
|
|
|
expires: number
|
|
|
|
|
fetched_at: number
|
|
|
|
|
properties: {
|
|
|
|
|
new_ignition: boolean
|
|
|
|
|
confidence: string
|
|
|
|
|
frp?: number
|
|
|
|
|
brightness?: number
|
|
|
|
|
acq_date: string
|
|
|
|
|
acq_time: string
|
|
|
|
|
near_fire?: string
|
|
|
|
|
distance_to_fire_km?: number
|
|
|
|
|
distance_km?: number
|
|
|
|
|
nearest_anchor?: string
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface HotspotsResponse {
|
|
|
|
|
enabled: boolean
|
|
|
|
|
hotspots: HotspotEvent[]
|
|
|
|
|
new_ignitions: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AvalancheResponse {
|
|
|
|
|
off_season: boolean
|
|
|
|
|
advisories: AvalancheEvent[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchFires(): Promise<FireEvent[]> {
|
|
|
|
|
return fetchJson<FireEvent[]>('/api/env/fires')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchAvalanche(): Promise<AvalancheResponse> {
|
|
|
|
|
return fetchJson<AvalancheResponse>('/api/env/avalanche')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchStreams(): Promise<StreamGaugeEvent[]> {
|
|
|
|
|
return fetchJson<StreamGaugeEvent[]>('/api/env/streams')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchTraffic(): Promise<TrafficEvent[]> {
|
|
|
|
|
return fetchJson<TrafficEvent[]>('/api/env/traffic')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchRoads(): Promise<RoadEvent[]> {
|
|
|
|
|
return fetchJson<RoadEvent[]>('/api/env/roads')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchHotspots(): Promise<HotspotsResponse> {
|
|
|
|
|
return fetchJson<HotspotsResponse>('/api/env/hotspots')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchRegions(): Promise<RegionInfo[]> {
|
|
|
|
|
return fetchJson<RegionInfo[]>('/api/regions')
|
|
|
|
|
}
|
2026-07-03 01:06:31 -06:00
|
|
|
|
|
|
|
|
export interface MeshcoreChannels { active: boolean; channels: string[] }
|
|
|
|
|
export interface TestSendResult { sent: boolean; detail: string }
|
|
|
|
|
|
|
|
|
|
export async function getMeshcoreChannels(): Promise<MeshcoreChannels> {
|
|
|
|
|
return fetchJson<MeshcoreChannels>('/api/meshcore/channels')
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 10:19:40 -06:00
|
|
|
// MeshCore channels with on-air hash + PSK key (routes by channel NAME, no slot/index).
|
|
|
|
|
export interface MeshcoreChannelDetail { name: string; hash: string | null; key: string | null }
|
2026-07-09 09:36:31 -06:00
|
|
|
export interface MeshcoreChannelsDetail {
|
|
|
|
|
active: boolean
|
|
|
|
|
channels: MeshcoreChannelDetail[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getMeshcoreChannelsDetail(): Promise<MeshcoreChannelsDetail> {
|
|
|
|
|
return fetchJson<MeshcoreChannelsDetail>('/api/meshcore/channels/detail')
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 03:55:36 +00:00
|
|
|
// Provision a new MeshCore channel on the companion. `key` is a 32-char hex
|
|
|
|
|
// PSK (16 bytes); omit it for a public channel (name must start with "#").
|
|
|
|
|
// Throws (with the backend's `detail` message) on failure — e.g. duplicate
|
|
|
|
|
// name, no free slot, bad key, or MeshCore not connected.
|
|
|
|
|
export async function addMeshcoreChannel(
|
|
|
|
|
name: string,
|
|
|
|
|
key?: string,
|
|
|
|
|
): Promise<MeshcoreChannels> {
|
|
|
|
|
const response = await fetch('/api/meshcore/channels', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ name, key: key || undefined }),
|
|
|
|
|
})
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const body = await response.json().catch(() => null)
|
|
|
|
|
throw new Error(body?.detail || `API error: ${response.status} ${response.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return response.json()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove a provisioned MeshCore channel from the companion by name.
|
|
|
|
|
export async function removeMeshcoreChannel(name: string): Promise<MeshcoreChannels> {
|
|
|
|
|
const response = await fetch(`/api/meshcore/channels/${encodeURIComponent(name)}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
})
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const body = await response.json().catch(() => null)
|
|
|
|
|
throw new Error(body?.detail || `API error: ${response.status} ${response.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return response.json()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 22:14:42 -06:00
|
|
|
// MeshCore room servers (type-3 contacts). A routing cell targets a room with
|
|
|
|
|
// the value ``room:<pubkey>`` (vs a bare channel name for channel targets).
|
|
|
|
|
// ``active:false`` / [] when MeshCore is not connected.
|
|
|
|
|
export interface MeshcoreRoom {
|
|
|
|
|
name: string | null
|
|
|
|
|
pubkey: string
|
|
|
|
|
prefix: string
|
|
|
|
|
path_established: boolean
|
2026-07-09 23:11:38 -06:00
|
|
|
password_set?: boolean
|
2026-07-09 22:14:42 -06:00
|
|
|
}
|
|
|
|
|
export interface MeshcoreRooms {
|
|
|
|
|
active: boolean
|
|
|
|
|
rooms: MeshcoreRoom[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getMeshcoreRooms(): Promise<MeshcoreRooms> {
|
|
|
|
|
return fetchJson<MeshcoreRooms>('/api/meshcore/rooms')
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 23:11:38 -06:00
|
|
|
// Set (or clear, if password is empty) a MeshCore room server's login password.
|
|
|
|
|
// The value is never read back — only a boolean status is returned.
|
|
|
|
|
export async function setRoomPassword(
|
|
|
|
|
pubkey: string,
|
|
|
|
|
password: string,
|
|
|
|
|
): Promise<{ ok: boolean; password_set: boolean }> {
|
|
|
|
|
const response = await fetch(`/api/meshcore/room-password/${encodeURIComponent(pubkey)}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ value: password }),
|
|
|
|
|
})
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`API error: ${response.status} ${response.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return response.json()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear a MeshCore room server's stored login password.
|
|
|
|
|
export async function clearRoomPassword(
|
|
|
|
|
pubkey: string,
|
|
|
|
|
): Promise<{ ok: boolean; password_set: boolean }> {
|
|
|
|
|
const response = await fetch(`/api/meshcore/room-password/${encodeURIComponent(pubkey)}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
})
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`API error: ${response.status} ${response.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return response.json()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 16:14:10 -06:00
|
|
|
export interface MeshcoreContact {
|
|
|
|
|
name: string | null
|
|
|
|
|
pubkey: string
|
2026-07-08 00:54:59 -06:00
|
|
|
type: number | null
|
2026-07-03 16:14:10 -06:00
|
|
|
last_advert: number | null
|
|
|
|
|
lat: number | null
|
|
|
|
|
lon: number | null
|
|
|
|
|
out_path_len: number | null
|
|
|
|
|
}
|
|
|
|
|
export interface MeshcoreContacts {
|
|
|
|
|
active: boolean
|
|
|
|
|
contacts: MeshcoreContact[]
|
|
|
|
|
}
|
|
|
|
|
export interface MeshcoreSelf {
|
|
|
|
|
name?: string | null
|
|
|
|
|
pubkey?: string | null
|
|
|
|
|
connected: boolean
|
|
|
|
|
host?: string
|
|
|
|
|
port?: number
|
|
|
|
|
channel_count?: number
|
|
|
|
|
last_advert_sent?: number | null // epoch seconds; null/absent = never advertised
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchMeshcoreContacts(): Promise<MeshcoreContacts> {
|
|
|
|
|
return fetchJson<MeshcoreContacts>('/api/meshcore/contacts')
|
|
|
|
|
}
|
|
|
|
|
export async function fetchMeshcoreSelf(): Promise<MeshcoreSelf> {
|
|
|
|
|
return fetchJson<MeshcoreSelf>('/api/meshcore/self')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function sendMeshcoreAdvert(): Promise<TestSendResult> {
|
|
|
|
|
const response = await fetch('/api/meshcore/advert', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({}),
|
|
|
|
|
})
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`API error: ${response.status} ${response.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return response.json()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 00:54:59 -06:00
|
|
|
// --- MeshCore telemetry ---
|
|
|
|
|
|
|
|
|
|
export interface MeshcoreTelemetryData {
|
|
|
|
|
voltage?: number; temperature?: number; humidity?: number; battery_pct?: number;
|
|
|
|
|
current?: number; illuminance?: number; barometer?: number; power?: number;
|
|
|
|
|
altitude?: number; distance?: number; gps?: unknown;
|
|
|
|
|
raw?: unknown[];
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
}
|
|
|
|
|
export interface MeshcoreTelemetryEntry {
|
|
|
|
|
contact: string;
|
|
|
|
|
data: MeshcoreTelemetryData | null;
|
|
|
|
|
polled_at: string | null;
|
|
|
|
|
available: boolean;
|
|
|
|
|
}
|
|
|
|
|
export interface MeshcoreTelemetry { active: boolean; entries: MeshcoreTelemetryEntry[]; }
|
|
|
|
|
export interface MeshcorePollResult { available: boolean; contact: string; data?: MeshcoreTelemetryData; detail?: string; }
|
|
|
|
|
|
|
|
|
|
// Connection config subset the telemetry UI reads/writes. The rest of the
|
|
|
|
|
// connection object is preserved verbatim via the index signature so PUTs can
|
|
|
|
|
// send the WHOLE object back (the backend coerces the body into the full
|
|
|
|
|
// ConnectionConfig dataclass — a partial PUT would reset omitted fields).
|
|
|
|
|
export interface ConnectionConfig {
|
|
|
|
|
meshcore_telemetry_contacts?: string[]
|
|
|
|
|
meshcore_telemetry_interval_seconds?: number
|
|
|
|
|
[key: string]: unknown
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchConnectionConfig(): Promise<ConnectionConfig> {
|
|
|
|
|
return fetchJson<ConnectionConfig>('/api/config/connection')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchMeshcoreTelemetry(): Promise<MeshcoreTelemetry> {
|
|
|
|
|
return fetchJson<MeshcoreTelemetry>('/api/meshcore/telemetry')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function pollMeshcoreContact(contact: string): Promise<MeshcorePollResult> {
|
|
|
|
|
const response = await fetch('/api/meshcore/telemetry/poll', {
|
|
|
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ contact }),
|
|
|
|
|
})
|
|
|
|
|
if (!response.ok) throw new Error(`API error: ${response.status} ${response.statusText}`)
|
|
|
|
|
return response.json()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 01:06:31 -06:00
|
|
|
export async function sendTestMessage(body: {
|
|
|
|
|
transport: 'meshtastic' | 'meshcore'
|
|
|
|
|
channel: string | number
|
|
|
|
|
text?: string
|
|
|
|
|
}): Promise<TestSendResult> {
|
|
|
|
|
const response = await fetch('/api/mesh/test-send', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
})
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`API error: ${response.status} ${response.statusText}`)
|
|
|
|
|
}
|
|
|
|
|
return response.json()
|
|
|
|
|
}
|