Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ radio:
sync_word: 0x2B
preamble_length: 16
tx_power_dbm: 22
# HAL GPS/PPS (RAK Pi HAT u-blox → concentrator PPS pin). Off by default.
# gps_pps_enabled: false
# gps_pps_tty_path: "/dev/ttyAMA0"
# gps_family: "ubx7"
# gps_pps_target_baud: 0 # 0 = HAL default (9600)

meshtastic:
default_key_b64: "AQ=="
Expand Down Expand Up @@ -50,7 +55,7 @@ device:
longitude: 0.0
altitude: 25
hardware_description: "RAK2287 + Raspberry Pi 4"
firmware_version: "0.7.6"
firmware_version: "0.7.5.1"

relay:
enabled: false
Expand All @@ -60,6 +65,16 @@ relay:
burst_size: 5
min_relay_rssi: -110.0
max_relay_rssi: -50.0
# blocklist: [] # 8-char hex node IDs — relay only, feed unchanged
# priority_list: [] # bypass burst gate under congestion
# dedup_ttl_seconds: 300
# storm_guard: # memory-only quarantine (PR 12)
# enabled: false
# window_seconds: 60
# identical_packet_threshold: 5
# rate_threshold_per_minute: 30
# quarantine_duration_seconds: 300
# notify_dashboard: true

upstream:
enabled: true
Expand All @@ -80,13 +95,6 @@ transmit:
nodeinfo:
interval_minutes: 180 # 5..1440 min, default 180 (3 hr); 0 = disabled
startup_delay_seconds: 60 # delay before first broadcast on boot
position:
interval_minutes: 15
startup_delay_seconds: 180
# LoRa mesh POSITION packets (Meshtastic app map). Meshradar fleet pin
# always uses device.latitude/longitude above, not live GPS.
coordinate_source: "static" # static | live (live needs gpsd/uart source)
location_precision: "approximate" # exact | approximate | none (live only)

web_auth:
# First-run state: hashes are empty -> dashboard forces /setup.
Expand Down Expand Up @@ -124,18 +132,23 @@ mqtt:
homeassistant_discovery: false

location:
# Where live GPS fixes come from (skyplot + optional mesh POSITION).
# Registered coordinates for Meshradar live in device.latitude/longitude
# and are not overwritten by gpsd.
# Where the Meshpoint's reported lat/lon/alt comes from.
# static : use device.latitude/longitude/altitude (default)
# gpsd : poll a local or remote gpsd daemon for live fixes
# uart : reserved (RAK Pi HAT GPS, not yet wired)
# uart : on-board RAK Pi HAT GPS via /dev/ttyAMA0 (NMEA GGA)
source: "static"
# gpsd connection. Defaults match the well-known local socket; only
# change when running gpsd on a peer machine on the LAN.
gpsd_host: "127.0.0.1"
gpsd_port: 2947
uart_path: "/dev/ttyAMA0"
uart_baud: 9600
# How often the coordinator wakes to read the active source.
update_interval_seconds: 5
# Minimum acceptable fix mode: 0=any (incl. no-fix), 1=2D, 2=3D.
min_fix_quality: 1

# signal_health:
# green_rssi_floor: -100 # badge green when 1h avg RSSI is above this
# yellow_rssi_floor: -115 # badge yellow above this, red below
# min_packets_per_hour: 5 # hide badge when fewer packets in the last hour
17 changes: 17 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,23 @@ relay:

The relay path is independent from RX: transmission never blocks packet reception. Packets are deduplicated by ID, rate-limited, and filtered by signal strength before relay.

### Storm guard quarantine (memory-only)

Temporary relay blocks for replay storms or excessive per-node packet rates. **Does not write to SQLite** and is separate from the permanent YAML blocklist. Quarantined nodes still appear in the live packet feed; only relay TX is blocked. Auto-releases after `quarantine_duration_seconds`. Operators can release early or promote to blocklist from **Configuration → Advanced**.

```yaml
relay:
storm_guard:
enabled: true
window_seconds: 60
identical_packet_threshold: 5 # same packet_id N times in window
rate_threshold_per_minute: 30 # packets from one node in window
quarantine_duration_seconds: 300
notify_dashboard: true # WS alert when PR 04 push notifications are present
```

**Interaction with other filters:** blocklist is checked first (permanent deny). Storm guard runs next (temporary deny). `rate_limited` applies only after a packet passes blocklist and quarantine. Busy legitimate nodes can false-positive if thresholds are too low — tune `rate_threshold_per_minute` for your mesh density.

---

## Transmit (Native Messaging)
Expand Down
67 changes: 54 additions & 13 deletions frontend/css/configuration.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,6 @@
line-height: 1.5;
}

.cfg-card__hint--nested {
margin: -4px 0 4px;
}

.cfg-inline-link {
color: var(--accent-cyan, #22d3ee);
text-decoration: none;
}

.cfg-inline-link:hover {
text-decoration: underline;
}

.cfg-form {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -242,6 +229,60 @@ select.cfg-field__input option:checked {
transform: translateX(-50%) translateY(0);
}

.cfg-id-list {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-bottom: 0.5rem;
}

.cfg-id-empty {
margin: 0;
font-size: 12px;
color: rgba(243, 244, 246, 0.45);
font-style: italic;
}

.cfg-id-row {
display: flex;
align-items: center;
gap: 0.5rem;
}

.cfg-id-chip {
font-family: 'JetBrains Mono', Menlo, monospace;
font-size: 12px;
padding: 0.15rem 0.45rem;
background: rgba(6, 182, 212, 0.1);
border: 1px solid rgba(6, 182, 212, 0.25);
border-radius: 6px;
color: #e2e8f0;
}

.cfg-id-remove {
background: transparent;
border: none;
color: rgba(243, 244, 246, 0.5);
font-size: 1.1rem;
line-height: 1;
cursor: pointer;
padding: 0 0.25rem;
}

.cfg-id-remove:hover {
color: #ef4444;
}

.cfg-id-add {
display: flex;
gap: 0.5rem;
align-items: center;
}

.cfg-id-add .cfg-field__input {
flex: 1;
}

@keyframes cfgFadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
Expand Down
68 changes: 68 additions & 0 deletions frontend/css/node_cards.css
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,74 @@
color: var(--accent-purple);
}

/* ---- Signal health sparkline + badge ---- */

.nc-card__health {
display: flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.3rem;
min-height: 28px;
}

.nc-health-badge {
flex-shrink: 0;
font-size: 0.55rem;
font-weight: 700;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.12rem 0.35rem;
border-radius: 999px;
line-height: 1.2;
}

.nc-health-badge--hidden {
display: none;
}

.nc-health-badge--green {
background: rgba(34, 197, 94, 0.18);
color: #22c55e;
border: 1px solid rgba(34, 197, 94, 0.35);
}

.nc-health-badge--yellow {
background: rgba(245, 158, 11, 0.18);
color: #f59e0b;
border: 1px solid rgba(245, 158, 11, 0.35);
}

.nc-health-badge--red {
background: rgba(239, 68, 68, 0.18);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.35);
}

.nc-quarantine-badge {
flex-shrink: 0;
font-size: 0.55rem;
font-weight: 700;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.03em;
padding: 0.12rem 0.35rem;
border-radius: 999px;
line-height: 1.2;
background: rgba(245, 158, 11, 0.22);
color: #f59e0b;
border: 1px solid rgba(245, 158, 11, 0.45);
}

.nc-sparkline {
flex: 1;
min-width: 0;
height: 28px;
width: 100%;
display: block;
opacity: 0.9;
}

/* ---- Card Rows ---- */

.nc-card__row {
Expand Down
4 changes: 4 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<link rel="stylesheet" href="css/settings.css" />
<link rel="stylesheet" href="css/configuration.css" />
<link rel="stylesheet" href="css/gps.css" />
<link rel="stylesheet" href="css/packet_detail_modal.css" />
<link rel="stylesheet" href="css/terminal_theme.css" />
<link rel="stylesheet" href="css/terminal.css" />
<link rel="stylesheet" href="/vendor/xterm/xterm.css" />
Expand Down Expand Up @@ -677,10 +678,12 @@ <h3 class="dangerous-panel__title">Service actions</h3>
<script src="js/meshpoint_display_units.js"></script>
<script src="js/meshpoint_node_favorites.js"></script>
<script src="js/node_cards_sort.js"></script>
<script src="js/signal_sparkline.js"></script>
<script src="js/node_cards.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<script src="js/node_metrics_chart.js"></script>
<script src="js/node_drawer.js"></script>
<script src="js/packet_detail_modal.js"></script>
<script src="js/simple_packet_feed.js"></script>
<script src="js/messaging_contacts.js"></script>
<script src="js/messaging_chat.js"></script>
Expand Down Expand Up @@ -718,6 +721,7 @@ <h3 class="dangerous-panel__title">Service actions</h3>
<script src="js/configuration/gps_stats_column.js"></script>
<script src="js/configuration/gps_card.js"></script>
<script src="js/configuration/advanced_card.js"></script>
<script src="js/configuration/relay_filters_card.js"></script>
<script src="js/configuration/configuration_panel.js"></script>
<script src="/vendor/xterm/xterm.js"></script>
<script src="/vendor/xterm/xterm-addon-fit.js"></script>
Expand Down
15 changes: 15 additions & 0 deletions frontend/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ document.addEventListener('DOMContentLoaded', async () => {
});

const nodeCards = new NodeCards('node-list', (node) => nodeDrawer.open(node));
window._nodeCards = nodeCards;

const backdrop = document.getElementById('node-backdrop');
if (backdrop) {
Expand All @@ -167,6 +168,7 @@ document.addEventListener('DOMContentLoaded', async () => {
});

await _loadInitial(nodeMap, nodeCards, packetFeed);
await _loadQuarantine(nodeCards);
await _updateStats();
_checkForUpdate();

Expand All @@ -181,6 +183,7 @@ document.addEventListener('DOMContentLoaded', async () => {

setInterval(() => {
_refreshData(nodeMap, nodeCards, packetFeed);
_loadQuarantine(nodeCards);
_updateStats();
}, 15_000);

Expand Down Expand Up @@ -324,6 +327,18 @@ async function _loadInitial(nodeMap, nodeList, packetFeed) {
}
}

async function _loadQuarantine(nodeCards) {
if (!nodeCards || typeof nodeCards.setQuarantine !== 'function') return;
try {
const res = await fetch('/api/relay/quarantine');
if (!res.ok) return;
const data = await res.json();
nodeCards.setQuarantine(data.entries || []);
} catch (e) {
console.error('Quarantine load failed:', e);
}
}

async function _refreshData(nodeMap, nodeList, packetFeed) {
try {
const [nodesRes, deviceRes] = await Promise.all([
Expand Down
12 changes: 10 additions & 2 deletions frontend/js/configuration/configuration_panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,18 @@ class ConfigurationPanel {
} else if (section === 'advanced' && window.AdvancedConfigCard) {
const host = document.getElementById('cfg-advanced-panel');
if (host) {
host.innerHTML = '';
host.innerHTML = `
<div data-cfg-advanced></div>
<div data-cfg-relay-filters></div>
`;
const card = new window.AdvancedConfigCard(api);
card.mount(host);
card.mount(host.querySelector('[data-cfg-advanced]'));
this._cards.set('advanced', card);
if (window.RelayFiltersCard) {
const relayCard = new window.RelayFiltersCard(api);
relayCard.mount(host.querySelector('[data-cfg-relay-filters]'));
this._cards.set('relay-filters', relayCard);
}
}
}
this._mounted.add(section);
Expand Down
Loading
Loading