some updates

This commit is contained in:
pablorevilla-meshtastic committed 2026-10-15 22:49:55 -07:00
1 parent 012150cba9
commit fb3a0154dd
7 files changed
+68 -22

No files matched your search

+3
View File
@@ -23,6 +23,9 @@ The project serves as a real-time monitoring and diagnostic tool for the Meshtas
### Version 3.0.6 — March 2026 ### Version 3.0.6 — March 2026
- Daily snapshots: added the `daily_snapshot` table, scheduled snapshot capture, and `/api/snapshots/daily` for historical node, packet, and gateway counts. - Daily snapshots: added the `daily_snapshot` table, scheduled snapshot capture, and `/api/snapshots/daily` for historical node, packet, and gateway counts.
Each new snapshot stores packets imported during the previous complete UTC day,
labeled with that day's date. Node and gateway counts are database totals at capture
time. Existing historical snapshots retain their previously stored packet totals.
- Stats/UI: expanded stats views with snapshot data and added channel filters to Chat and Firehose. - Stats/UI: expanded stats views with snapshot data and added channel filters to Chat and Firehose.
- Map/node fixes: corrected node location handling, map popup behavior, packet page display, and node list sorting. - Map/node fixes: corrected node location handling, map popup behavior, packet page display, and node list sorting.
- Ingestion/API reliability: improved duplicate `PacketSeen` handling, guarded invalid node info packets, and made traceroute/edge APIs more tolerant of decode failures. - Ingestion/API reliability: improved duplicate `PacketSeen` handling, guarded invalid node info packets, and made traceroute/edge APIs more tolerant of decode failures.
+23
View File
@@ -227,9 +227,32 @@ Response Example
## 6. Config API ## 6. Config API
### GET `/api/snapshots/daily`
Returns stored daily snapshots. For new snapshots, `packet_count` is the number of
packet records imported during `date` (00:00 UTC inclusive to the next midnight
exclusive). The scheduled capture records the previous complete UTC day. Repeated
observations of the same packet are not counted separately. Node and gateway counts
remain database totals at capture time. Older snapshots retain the cumulative packet
counts originally stored; they are not automatically converted.
### GET `/api/config` ### GET `/api/config`
Returns a safe subset of server configuration. Returns a safe subset of server configuration.
`site.map_channel_colors` contains channel color overrides from the optional
`[map_channel_colors]` INI section. Each entry is `channel = #RRGGBB`; invalid colors
are omitted. Channel matching on the map ignores case, spaces, hyphens, and underscores.
Unlisted channels use generated colors. For example:
```ini
[map_channel_colors]
LongFast = #4363d8
LocalChat = #18aa18
```
Restart the web server and reload the map after changing these settings. Omitting
the section uses generated colors for every channel.
Response Example Response Example
```json ```json
{ {
+14 -5
View File
@@ -1,7 +1,7 @@
import logging import logging
import re import re
import time import time
from datetime import UTC, datetime from datetime import UTC, datetime, timedelta
from sqlalchemy import func, select, update from sqlalchemy import func, select, update
from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.dialects.postgresql import insert as pg_insert
@@ -110,12 +110,21 @@ async def ensure_node_exists(session, node_id: int | None, channel: str | None)
async def capture_daily_snapshot() -> None: async def capture_daily_snapshot() -> None:
today = datetime.now(UTC).date() """Record packet traffic for the previous complete UTC day."""
day_end = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
day_start = day_end - timedelta(days=1)
start_us = int(day_start.timestamp() * 1_000_000)
end_us = int(day_end.timestamp() * 1_000_000)
snapshot_date = day_start.date()
async with mqtt_database.async_session() as session: async with mqtt_database.async_session() as session:
node_count = (await session.execute(select(func.count()).select_from(Node))).scalar_one() node_count = (await session.execute(select(func.count()).select_from(Node))).scalar_one()
packet_count = ( packet_count = (
await session.execute(select(func.count()).select_from(Packet)) await session.execute(
select(func.count())
.select_from(Packet)
.where(Packet.import_time_us >= start_us, Packet.import_time_us < end_us)
)
).scalar_one() ).scalar_one()
gateway_count = ( gateway_count = (
await session.execute( await session.execute(
@@ -124,7 +133,7 @@ async def capture_daily_snapshot() -> None:
).scalar_one() ).scalar_one()
captured_at_us = int(time.time() * 1_000_000) captured_at_us = int(time.time() * 1_000_000)
values = { values = {
"snapshot_date": today, "snapshot_date": snapshot_date,
"node_count": node_count, "node_count": node_count,
"packet_count": packet_count, "packet_count": packet_count,
"gateway_count": gateway_count, "gateway_count": gateway_count,
@@ -151,7 +160,7 @@ async def capture_daily_snapshot() -> None:
else: else:
snapshot = ( snapshot = (
await session.execute( await session.execute(
select(DailySnapshot).where(DailySnapshot.snapshot_date == today) select(DailySnapshot).where(DailySnapshot.snapshot_date == snapshot_date)
) )
).scalar_one_or_none() ).scalar_one_or_none()
if snapshot is None: if snapshot is None:
+8 -16
View File
@@ -275,23 +275,11 @@ const PACKET_LANDING_DURATION_MS = 1000;
const portMap = window.PORT_LABEL_MAP; const portMap = window.PORT_LABEL_MAP;
// Fallback colors for channels that are not Meshtastic presets. // Generated colors for channels without a configured override.
// Every entry clears 3:1 contrast against both the light map tiles and the dark
// filter panel, and stays perceptually far from every channelColorOverrides value.
const palette = ["#18aa18","#976049","#a5508c","#1898dc","#3a8846","#dc1884", const palette = ["#18aa18","#976049","#a5508c","#1898dc","#3a8846","#dc1884",
"#b8813d","#b64f35","#408b18","#4c719e","#be379c","#18aa52"]; "#b8813d","#b64f35","#408b18","#4c719e","#be379c","#18aa52"];
const channelColorOverrides = { const channelColorOverrides = new Map();
longfast: "#4363d8",
longslow: "#000075",
verylongslow: "#808000",
mediumslow: "#008080",
mediumfast: "#e6194b",
shortslow: "#911eb4",
shortfast: "#f58231",
longmoderate: "#46f0f0",
shortturbo: "#f032e6"
};
const colorMap = new Map(); const colorMap = new Map();
const channelSet = new Set(); const channelSet = new Set();
@@ -316,7 +304,7 @@ function hashToColor(str){
const channel = String(str || "Unknown"); const channel = String(str || "Unknown");
if(colorMap.has(channel)) return colorMap.get(channel); if(colorMap.has(channel)) return colorMap.get(channel);
const override = channelColorOverrides[normalizeChannelName(channel)]; const override = channelColorOverrides.get(normalizeChannelName(channel));
const c = override || palette[Math.floor(hashToUnit(channel) * palette.length) % palette.length]; const c = override || palette[Math.floor(hashToUnit(channel) * palette.length) % palette.length];
colorMap.set(channel,c); colorMap.set(channel,c);
return c; return c;
@@ -477,6 +465,9 @@ async function waitForConfig() {
} }
try { try {
const cfg = await window._siteConfigPromise; const cfg = await window._siteConfigPromise;
for(const [channel, color] of Object.entries(cfg.site?.map_channel_colors || {})){
channelColorOverrides.set(normalizeChannelName(channel), color);
}
return cfg.site || {}; return cfg.site || {};
} catch (err) { } catch (err) {
console.error("Error loading site config:", err); console.error("Error loading site config:", err);
@@ -525,7 +516,8 @@ initMapPolling();
fetch('/api/nodes?days_active=3') fetch('/api/nodes?days_active=3')
.then(r=>r.json()) .then(r=>r.json())
.then(data=>{ .then(async data=>{
await waitForConfig();
if(!data.nodes) return; if(!data.nodes) return;
nodes = data.nodes.map(n=>({ nodes = data.nodes.map(n=>({
+1 -1
View File
@@ -535,7 +535,7 @@ seenTableBody.innerHTML = Object.keys(hopGroups)
Escape HTML helper Escape HTML helper
----------------------------------------------*/ ----------------------------------------------*/
function escapeHtml(unsafe) { function escapeHtml(unsafe) {
return (unsafe ?? "").replace(/[&<"'>]/g, m => ({ return String(unsafe ?? "").replace(/[&<"'>]/g, m => ({
"&":"&amp;", "&":"&amp;",
"<":"&lt;", "<":"&lt;",
">":"&gt;", ">":"&gt;",
+6
View File
@@ -744,6 +744,12 @@ async def api_config(request):
"map_bottom_right_lat": get_float(site, "map_bottom_right_lat", 36.0), "map_bottom_right_lat": get_float(site, "map_bottom_right_lat", 36.0),
"map_bottom_right_lon": get_float(site, "map_bottom_right_lon", -121.0), "map_bottom_right_lon": get_float(site, "map_bottom_right_lon", -121.0),
"map_interval": get_int(site, "map_interval", 3), "map_interval": get_int(site, "map_interval", 3),
"map_channel_colors": {
channel: color.strip()
for channel, color in CONFIG.get("map_channel_colors", {}).items()
if re.fullmatch(r"#[0-9a-fA-F]{6}", color.strip())
},
"firehose_interval": get_int(site, "firehose_interval", 3), "firehose_interval": get_int(site, "firehose_interval", 3),
"weekly_net_message": get_str( "weekly_net_message": get_str(
site, "weekly_net_message", "Weekly Mesh check-in message." site, "weekly_net_message", "Weekly Mesh check-in message."
+13
View File
@@ -59,6 +59,19 @@ firehose_interal=3
weekly_net_message = Weekly Mesh check-in. We will keep it open on every Wednesday from 5:00pm for checkins. The message format should be (LONG NAME) - (CITY YOU ARE IN) #BayMeshNet. weekly_net_message = Weekly Mesh check-in. We will keep it open on every Wednesday from 5:00pm for checkins. The message format should be (LONG NAME) - (CITY YOU ARE IN) #BayMeshNet.
net_tag = #BayMeshNet net_tag = #BayMeshNet
# Map channel color overrides. Use six-digit hex colors; other channels get generated colors.
# Names ignore case, spaces, hyphens, and underscores. Restart the web server after editing.
[map_channel_colors]
LongFast = #4363d8
LongSlow = #000075
VeryLongSlow = #808000
MediumSlow = #008080
MediumFast = #e6194b
ShortSlow = #911eb4
ShortFast = #f58231
LongModerate = #46f0f0
ShortTurbo = #f032e6
# ------------------------- # -------------------------
# MQTT Broker Configuration # MQTT Broker Configuration
# ------------------------- # -------------------------