mirror of
https://github.com/yellowcooln/meshcore-mqtt-live-map.git
synced 2026-04-20 23:23:36 +00:00
Sync dev updates and versioning
This commit is contained in:
parent
21fd17396c
commit
f360e64fed
14 changed files with 264 additions and 3 deletions
|
|
@ -19,6 +19,7 @@ SITE_OG_IMAGE=
|
|||
SITE_URL=https://your-domain.example/
|
||||
SITE_ICON=/static/logo.png
|
||||
SITE_FEED_NOTE=Feed: MQTT broker.
|
||||
CUSTOM_LINK_URL=
|
||||
DISTANCE_UNITS=km
|
||||
NODE_MARKER_RADIUS=8
|
||||
HISTORY_LINK_SCALE=1
|
||||
|
|
@ -46,8 +47,14 @@ ROUTE_HISTORY_FILE=/data/route_history.jsonl
|
|||
ROUTE_HISTORY_PAYLOAD_TYPES=8,9,2,5,4
|
||||
MQTT_ONLINE_SECONDS=300
|
||||
MQTT_ONLINE_TOPIC_SUFFIXES=/status,/packets
|
||||
MQTT_ONLINE_FORCE_NAMES=
|
||||
MQTT_SEEN_BROADCAST_MIN_SECONDS=5
|
||||
|
||||
GIT_CHECK_ENABLED=false
|
||||
GIT_CHECK_FETCH=false
|
||||
GIT_CHECK_PATH=/repo
|
||||
GIT_CHECK_INTERVAL_SECONDS=43200
|
||||
|
||||
MAP_START_LAT=42.3601
|
||||
MAP_START_LON=-71.1500
|
||||
MAP_START_ZOOM=10
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
- `docker-compose.yaml` runs the service as `meshmap-live`.
|
||||
- `data/` stores persisted state (`state.json`), route history (`route_history.jsonl`), and optional role overrides (`device_roles.json`).
|
||||
- `.env` holds dev runtime settings; `.env.example` mirrors template defaults.
|
||||
- `VERSION.txt` tracks the current version; append changes in `VERSIONS.md`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `docker compose up -d --build` rebuilds and restarts the backend (preferred workflow).
|
||||
|
|
@ -46,6 +47,9 @@
|
|||
- `MAP_RADIUS_SHOW=true` draws a debug circle centered on `MAP_START_LAT/LON`.
|
||||
- Set `TRAIL_LEN=0` to disable trails entirely; the HUD trail hint is removed when trails are off.
|
||||
- Coverage button only appears when `COVERAGE_API_URL` is set.
|
||||
- Optional custom HUD link appears when `CUSTOM_LINK_URL` is set.
|
||||
- Update banner uses `GIT_CHECK_ENABLED` (compare local vs upstream) with `GIT_CHECK_PATH` pointing at a git repo.
|
||||
- `GIT_CHECK_FETCH` controls whether the server fetches before comparing; `GIT_CHECK_INTERVAL_SECONDS` sets the recheck interval.
|
||||
- Route history modes default to `path,direct,fanout` via `ROUTE_HISTORY_ALLOWED_MODES`.
|
||||
- `ROUTE_PATH_MAX_LEN` caps oversized path-hash lists (prevents bogus long routes).
|
||||
- Persisted state in `data/state.json` is loaded on startup; edit with care.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ Thanks for helping improve the MeshCore Live Map. This repo is intentionally lig
|
|||
2) Rebuild: `docker compose up -d --build`
|
||||
3) Verify: `curl -s http://localhost:8080/snapshot`
|
||||
|
||||
## Versioning
|
||||
- Update `VERSION.txt` when adding features.
|
||||
- Append a new section to `VERSIONS.md` describing the change set.
|
||||
|
||||
## Project Layout
|
||||
- `backend/app.py` handles MQTT ingest, decoding, routing, and API endpoints.
|
||||
- `backend/static/index.html` is the HTML shell + template placeholders.
|
||||
|
|
@ -26,6 +30,8 @@ Thanks for helping improve the MeshCore Live Map. This repo is intentionally lig
|
|||
- If `COVERAGE_API_URL` is blank, confirm the Coverage button is hidden.
|
||||
- Note: coordinates at `0,0` (even as strings) are filtered and won’t render.
|
||||
- Radius filter: `MAP_RADIUS_KM` defaults to 241.4 km (150mi); set `0` to disable.
|
||||
- `CUSTOM_LINK_URL` shows an extra HUD link when set; leave blank to hide.
|
||||
- Update banner uses `GIT_CHECK_ENABLED` + `GIT_CHECK_PATH` to compare local vs upstream.
|
||||
|
||||
## UI Changes
|
||||
When adding UI controls:
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -1,12 +1,13 @@
|
|||
# Mesh Live Map
|
||||
|
||||
Version: `1.0.1` (see `VERSION.txt`)
|
||||
|
||||
Live MeshCore traffic map that renders nodes, routes, and activity in real time on a Leaflet map. The backend subscribes to MQTT over WebSockets + TLS, decodes MeshCore packets with `@michaelhart/meshcore-decoder`, and streams updates to the browser via WebSockets.
|
||||
|
||||
Live example sites:
|
||||
- https://live.bostonme.sh/ - Greater Boston Mesh Map
|
||||
- https://map.eastmesh.au/ - Aus Eastern Mesh Live Map
|
||||
- https://mesh-map.e-l33t.org/ - NSW Mesh - Live Mesh Traffic Map
|
||||
Update check marker: 2025-01-11
|
||||
|
||||
|
||||

|
||||
|
|
@ -22,6 +23,7 @@ Update check marker: 2025-01-11
|
|||
- 24-hour route history tool with volume-based coloring, click-to-view packet details, a heat-band slider, and a link-size slider
|
||||
- Peers tool showing incoming/outgoing neighbors with on-map lines
|
||||
- Coverage layer from a coverage map API (button hidden when not configured)
|
||||
- Update available banner (git local vs upstream) with dismiss
|
||||
- UI controls: legend toggle, dark map, topo map, units toggle (km/mi), labels toggle, hide nodes, heat toggle
|
||||
- Share button that copies a URL with current view + settings
|
||||
- URL parameters to open the map at a specific view (center, zoom, toggles)
|
||||
|
|
@ -84,6 +86,7 @@ Site metadata (page title + embeds):
|
|||
- `SITE_URL` (public URL)
|
||||
- `SITE_ICON`
|
||||
- `SITE_FEED_NOTE`
|
||||
- `CUSTOM_LINK_URL` (optional extra HUD link; hidden when blank)
|
||||
- `DISTANCE_UNITS` (`km` or `mi`, default display units)
|
||||
- `NODE_MARKER_RADIUS` (default node marker size in pixels)
|
||||
|
||||
|
|
@ -124,6 +127,12 @@ Heat + online status:
|
|||
- `MQTT_SEEN_BROADCAST_MIN_SECONDS`
|
||||
- `MQTT_ONLINE_FORCE_NAMES` (comma-separated names to force as MQTT online; also excluded from peers)
|
||||
|
||||
Update checks:
|
||||
- `GIT_CHECK_ENABLED` (show update banner if repo is behind)
|
||||
- `GIT_CHECK_FETCH` (fetch before comparing)
|
||||
- `GIT_CHECK_PATH` (path to git repo in the container)
|
||||
- `GIT_CHECK_INTERVAL_SECONDS` (defaults to 43200 = 12h)
|
||||
|
||||
Map + LOS:
|
||||
- `MAP_START_LAT` / `MAP_START_LON` / `MAP_START_ZOOM` (default map view)
|
||||
- `MAP_DEFAULT_LAYER` (`light`, `dark`, or `topo`; localStorage overrides)
|
||||
|
|
|
|||
1
VERSION.txt
Normal file
1
VERSION.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
1.0.1
|
||||
22
VERSIONS.md
Normal file
22
VERSIONS.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Versions
|
||||
|
||||
## v1.0.1 (01-11-2025)
|
||||
- Update check banner (git local vs upstream) with dismiss + auto recheck every 12 hours
|
||||
- Custom HUD link button (configurable via env, hidden when unset)
|
||||
- Update banner rendered from HTML dataset to avoid JS/token fetch issues
|
||||
- Git repo mounted into container for update checks; safe.directory configured automatically
|
||||
- Update banner Hide button styled to match HUD controls
|
||||
|
||||
## v1.0.0 (01-10-2025)
|
||||
- Live MeshCore node map with MQTT ingest, websocket updates, and Leaflet UI
|
||||
- Node markers with roles, names, and MQTT online ring
|
||||
- Trace/path, message, and advert route lines with animations
|
||||
- Heatmap for recent activity (toggle + intensity slider)
|
||||
- 24h history tool with heat filter + link weight slider
|
||||
- Peers tool showing inbound/outbound neighbors with map lines
|
||||
- LOS tool with elevation profile, peaks, relay suggestion, and mobile support
|
||||
- Propagation tool with right-side panel and map overlay
|
||||
- Search, labels toggle, hide nodes, map layer toggles, and shareable URL params
|
||||
- Distance unit toggle (km/mi) with per-user preference
|
||||
- PWA install support (manifest + service worker)
|
||||
- Persistent state + route history on disk
|
||||
|
|
@ -3,7 +3,7 @@ FROM python:3.12-slim
|
|||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nodejs npm \
|
||||
&& apt-get install -y --no-install-recommends nodejs npm git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import json
|
|||
import os
|
||||
import html
|
||||
import time
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from dataclasses import asdict
|
||||
from typing import Any, Dict, Optional, Set, List
|
||||
|
|
@ -98,6 +99,11 @@ from config import (
|
|||
SITE_URL,
|
||||
SITE_ICON,
|
||||
SITE_FEED_NOTE,
|
||||
CUSTOM_LINK_URL,
|
||||
GIT_CHECK_ENABLED,
|
||||
GIT_CHECK_FETCH,
|
||||
GIT_CHECK_PATH,
|
||||
GIT_CHECK_INTERVAL_SECONDS,
|
||||
DISTANCE_UNITS,
|
||||
NODE_MARKER_RADIUS,
|
||||
HISTORY_LINK_SCALE,
|
||||
|
|
@ -154,6 +160,14 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
|
|||
mqtt_client: Optional[mqtt.Client] = None
|
||||
clients: Set[WebSocket] = set()
|
||||
update_queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue()
|
||||
git_update_info = {
|
||||
"available": False,
|
||||
"local": None,
|
||||
"remote": None,
|
||||
"local_short": None,
|
||||
"remote_short": None,
|
||||
"error": None,
|
||||
}
|
||||
|
||||
# =========================
|
||||
# Helpers: coordinate hunting
|
||||
|
|
@ -192,6 +206,71 @@ def _serialize_state() -> Dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def _check_git_updates() -> None:
|
||||
if not GIT_CHECK_ENABLED:
|
||||
return
|
||||
|
||||
if not GIT_CHECK_PATH or not os.path.isdir(GIT_CHECK_PATH):
|
||||
git_update_info["error"] = "git_path_missing"
|
||||
return
|
||||
|
||||
def _run_git(args: List[str]) -> str:
|
||||
result = subprocess.run(
|
||||
args,
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "config", "--global", "--add", "safe.directory", GIT_CHECK_PATH],
|
||||
check=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
inside = _run_git(["git", "-C", GIT_CHECK_PATH, "rev-parse", "--is-inside-work-tree"])
|
||||
if inside.lower() != "true":
|
||||
git_update_info["error"] = "not_git_repo"
|
||||
return
|
||||
except Exception:
|
||||
git_update_info["error"] = "git_unavailable"
|
||||
return
|
||||
|
||||
try:
|
||||
if GIT_CHECK_FETCH:
|
||||
subprocess.run(
|
||||
["git", "-C", GIT_CHECK_PATH, "fetch", "--quiet", "--prune"],
|
||||
check=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
local_sha = _run_git(["git", "-C", GIT_CHECK_PATH, "rev-parse", "HEAD"])
|
||||
remote_sha = _run_git(["git", "-C", GIT_CHECK_PATH, "rev-parse", "@{u}"])
|
||||
git_update_info["local"] = local_sha
|
||||
git_update_info["remote"] = remote_sha
|
||||
git_update_info["local_short"] = local_sha[:7]
|
||||
git_update_info["remote_short"] = remote_sha[:7]
|
||||
git_update_info["available"] = local_sha != remote_sha
|
||||
if git_update_info["available"]:
|
||||
print(f"[update] available {git_update_info['local_short']} -> {git_update_info['remote_short']}")
|
||||
except Exception:
|
||||
git_update_info["error"] = "git_compare_failed"
|
||||
|
||||
|
||||
async def _git_check_loop() -> None:
|
||||
if not GIT_CHECK_ENABLED:
|
||||
return
|
||||
if GIT_CHECK_INTERVAL_SECONDS <= 0:
|
||||
return
|
||||
while True:
|
||||
await asyncio.sleep(GIT_CHECK_INTERVAL_SECONDS)
|
||||
_check_git_updates()
|
||||
|
||||
|
||||
def _device_payload(device_id: str, state: "DeviceState") -> Dict[str, Any]:
|
||||
payload = asdict(state)
|
||||
last_seen = seen_devices.get(device_id)
|
||||
|
|
@ -1069,6 +1148,7 @@ def root():
|
|||
"SITE_URL": SITE_URL,
|
||||
"SITE_ICON": SITE_ICON,
|
||||
"SITE_FEED_NOTE": SITE_FEED_NOTE,
|
||||
"CUSTOM_LINK_URL": CUSTOM_LINK_URL,
|
||||
"DISTANCE_UNITS": DISTANCE_UNITS,
|
||||
"NODE_MARKER_RADIUS": NODE_MARKER_RADIUS,
|
||||
"HISTORY_LINK_SCALE": HISTORY_LINK_SCALE,
|
||||
|
|
@ -1088,6 +1168,10 @@ def root():
|
|||
"LOS_PEAKS_MAX": LOS_PEAKS_MAX,
|
||||
"MQTT_ONLINE_SECONDS": MQTT_ONLINE_SECONDS,
|
||||
"COVERAGE_API_URL": COVERAGE_API_URL,
|
||||
"UPDATE_AVAILABLE": str(bool(git_update_info.get("available"))).lower(),
|
||||
"UPDATE_LOCAL": git_update_info.get("local_short") or "",
|
||||
"UPDATE_REMOTE": git_update_info.get("remote_short") or "",
|
||||
"UPDATE_BANNER_HIDDEN": "" if git_update_info.get("available") else "hidden",
|
||||
}
|
||||
for key, value in replacements.items():
|
||||
safe_value = html.escape(str(value), quote=True)
|
||||
|
|
@ -1147,6 +1231,7 @@ def snapshot(request: Request):
|
|||
"history_edges": [_history_edge_payload(e) for e in route_history_edges.values()],
|
||||
"history_window_seconds": int(max(0, ROUTE_HISTORY_HOURS * 3600)),
|
||||
"heat": _serialize_heat_events(),
|
||||
"update": git_update_info,
|
||||
"server_time": time.time(),
|
||||
}
|
||||
|
||||
|
|
@ -1472,6 +1557,7 @@ async def ws_endpoint(ws: WebSocket):
|
|||
"history_edges": [_history_edge_payload(e) for e in route_history_edges.values()],
|
||||
"history_window_seconds": int(max(0, ROUTE_HISTORY_HOURS * 3600)),
|
||||
"heat": _serialize_heat_events(),
|
||||
"update": git_update_info,
|
||||
}))
|
||||
|
||||
try:
|
||||
|
|
@ -1495,6 +1581,7 @@ async def startup():
|
|||
_load_state()
|
||||
_load_route_history()
|
||||
_ensure_node_decoder()
|
||||
_check_git_updates()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
transport = "websockets" if MQTT_TRANSPORT == "websockets" else "tcp"
|
||||
|
|
@ -1537,6 +1624,7 @@ async def startup():
|
|||
asyncio.create_task(reaper())
|
||||
asyncio.create_task(_state_saver())
|
||||
asyncio.create_task(_route_history_saver())
|
||||
asyncio.create_task(_git_check_loop())
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
|
|
|
|||
|
|
@ -70,6 +70,14 @@ SITE_OG_IMAGE = os.getenv("SITE_OG_IMAGE", "")
|
|||
SITE_URL = os.getenv("SITE_URL", "/")
|
||||
SITE_ICON = os.getenv("SITE_ICON", "/static/logo.png")
|
||||
SITE_FEED_NOTE = os.getenv("SITE_FEED_NOTE", "Feed: Boston MQTT.")
|
||||
CUSTOM_LINK_URL = os.getenv("CUSTOM_LINK_URL", "").strip()
|
||||
GIT_CHECK_ENABLED = os.getenv("GIT_CHECK_ENABLED", "false").lower() == "true"
|
||||
GIT_CHECK_FETCH = os.getenv("GIT_CHECK_FETCH", "false").lower() == "true"
|
||||
GIT_CHECK_PATH = os.getenv("GIT_CHECK_PATH", os.getcwd()).strip()
|
||||
try:
|
||||
GIT_CHECK_INTERVAL_SECONDS = float(os.getenv("GIT_CHECK_INTERVAL_SECONDS", "43200"))
|
||||
except ValueError:
|
||||
GIT_CHECK_INTERVAL_SECONDS = 43200.0
|
||||
DISTANCE_UNITS = os.getenv("DISTANCE_UNITS", "km").strip().lower()
|
||||
if DISTANCE_UNITS not in ("km", "mi"):
|
||||
DISTANCE_UNITS = "km"
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@
|
|||
const queryHistoryFilter = parseHistoryFilterParam(
|
||||
queryParams.get('history_filter') || queryParams.get('historyFilter') || queryParams.get('historyfilter')
|
||||
);
|
||||
const initialUpdateAvailable = parseBoolParam(config.updateAvailable);
|
||||
const initialUpdateLocal = (config.updateLocal || '').trim();
|
||||
const initialUpdateRemote = (config.updateRemote || '').trim();
|
||||
const reportError = typeof window.__meshmapReportError === 'function'
|
||||
? window.__meshmapReportError
|
||||
: (message) => console.warn(message);
|
||||
|
|
@ -142,6 +145,7 @@
|
|||
const HEAT_TTL_MS = 10 * 60 * 1000;
|
||||
const losLayer = L.layerGroup().addTo(map);
|
||||
const coverageApiUrl = (config.coverageApiUrl || '').trim();
|
||||
const customLinkUrl = (config.customLinkUrl || '').trim();
|
||||
const coverageEnabled = Boolean(coverageApiUrl);
|
||||
const coverageLayer = L.layerGroup();
|
||||
let coverageVisible = false;
|
||||
|
|
@ -3346,6 +3350,9 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
historyWindowSeconds = Number(snap.history_window_seconds);
|
||||
updateHistoryWindowLabel(historyWindowSeconds);
|
||||
}
|
||||
if (snap.update) {
|
||||
setUpdateBanner(snap.update);
|
||||
}
|
||||
setStats();
|
||||
} catch (e) {
|
||||
console.warn("snapshot failed", e);
|
||||
|
|
@ -3386,6 +3393,9 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
historyWindowSeconds = Number(msg.history_window_seconds);
|
||||
updateHistoryWindowLabel(historyWindowSeconds);
|
||||
}
|
||||
if (msg.update) {
|
||||
setUpdateBanner(msg.update);
|
||||
}
|
||||
setStats();
|
||||
return;
|
||||
}
|
||||
|
|
@ -3509,6 +3519,58 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
}
|
||||
}
|
||||
|
||||
const customLink = document.getElementById('custom-link');
|
||||
const updateBanner = document.getElementById('update-banner');
|
||||
const updateText = document.getElementById('update-text');
|
||||
const updateDismiss = document.getElementById('update-dismiss');
|
||||
let updateDismissKey = null;
|
||||
const setUpdateBanner = (info) => {
|
||||
if (!updateBanner) return;
|
||||
if (!info || !info.available) {
|
||||
updateBanner.hidden = true;
|
||||
updateDismissKey = null;
|
||||
return;
|
||||
}
|
||||
const remoteKey = info.remote_short || info.remote || 'update';
|
||||
updateDismissKey = remoteKey;
|
||||
const dismissed = localStorage.getItem('meshmapUpdateDismissed');
|
||||
if (dismissed && dismissed === remoteKey) {
|
||||
updateBanner.hidden = true;
|
||||
return;
|
||||
}
|
||||
const localLabel = info.local_short || info.local || 'local';
|
||||
const remoteLabel = info.remote_short || info.remote || 'remote';
|
||||
if (updateText) {
|
||||
updateText.textContent = `Update available (${localLabel} \u2192 ${remoteLabel})`;
|
||||
}
|
||||
updateBanner.hidden = false;
|
||||
};
|
||||
if (customLink) {
|
||||
if (customLinkUrl) {
|
||||
customLink.setAttribute('href', customLinkUrl);
|
||||
customLink.setAttribute('title', customLinkUrl);
|
||||
} else {
|
||||
customLink.remove();
|
||||
}
|
||||
}
|
||||
if (updateDismiss) {
|
||||
updateDismiss.addEventListener('click', () => {
|
||||
if (updateDismissKey) {
|
||||
localStorage.setItem('meshmapUpdateDismissed', updateDismissKey);
|
||||
}
|
||||
if (updateBanner) updateBanner.hidden = true;
|
||||
});
|
||||
}
|
||||
if (initialUpdateAvailable) {
|
||||
setUpdateBanner({
|
||||
available: true,
|
||||
local_short: initialUpdateLocal || null,
|
||||
remote_short: initialUpdateRemote || null,
|
||||
local: initialUpdateLocal || null,
|
||||
remote: initialUpdateRemote || null,
|
||||
});
|
||||
}
|
||||
|
||||
const shareToggle = document.getElementById('share-toggle');
|
||||
if (shareToggle) {
|
||||
const resetShareButton = () => {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@
|
|||
data-node-radius="{{NODE_MARKER_RADIUS}}"
|
||||
data-history-link-scale="{{HISTORY_LINK_SCALE}}"
|
||||
data-coverage-api-url="{{COVERAGE_API_URL}}"
|
||||
data-custom-link-url="{{CUSTOM_LINK_URL}}"
|
||||
data-update-available="{{UPDATE_AVAILABLE}}"
|
||||
data-update-local="{{UPDATE_LOCAL}}"
|
||||
data-update-remote="{{UPDATE_REMOTE}}"
|
||||
>
|
||||
<script>
|
||||
window.__meshmapStarted = false;
|
||||
|
|
@ -64,6 +68,12 @@
|
|||
<path d="M18 16.1a2.9 2.9 0 0 0-1.95.77l-7.1-4.14c.04-.23.05-.46.05-.7 0-.23-.01-.46-.05-.68l7.05-4.12A2.95 2.95 0 1 0 14.5 5c0 .23.02.46.07.68L7.52 9.8a2.9 2.9 0 1 0 0 4.4l7.05 4.12a2.9 2.9 0 1 0 3.43-2.22z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<a class="hud-action hud-custom" id="custom-link" href="{{CUSTOM_LINK_URL}}" target="_blank" rel="noopener" aria-label="Open custom link">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M14 3h7v7h-2V6.41l-9.3 9.3-1.4-1.42 9.3-9.29H14V3z"/>
|
||||
<path d="M5 5h6v2H7v10h10v-4h2v6H5V5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="hud-action hud-github" href="https://github.com/yellowcooln/meshcore-mqtt-live-map/" target="_blank" rel="noopener" aria-label="GitHub repository">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 2C6.48 2 2 6.58 2 12.26c0 4.5 2.87 8.32 6.84 9.67.5.1.68-.23.68-.5v-1.77c-2.78.62-3.37-1.38-3.37-1.38-.46-1.2-1.12-1.52-1.12-1.52-.9-.64.07-.63.07-.63 1 .07 1.53 1.07 1.53 1.07.9 1.6 2.36 1.14 2.94.87.1-.66.35-1.14.63-1.4-2.22-.27-4.56-1.14-4.56-5.08 0-1.12.39-2.04 1.02-2.76-.1-.27-.45-1.37.1-2.86 0 0 .83-.27 2.72 1.05a9.2 9.2 0 0 1 2.48-.35c.84 0 1.68.12 2.48.35 1.88-1.32 2.7-1.05 2.7-1.05.56 1.5.21 2.6.11 2.86.64.72 1.02 1.64 1.02 2.76 0 3.95-2.34 4.8-4.57 5.07.36.33.68.97.68 1.96v2.9c0 .28.18.6.69.5A10.03 10.03 0 0 0 22 12.26C22 6.58 17.52 2 12 2z"/>
|
||||
|
|
@ -74,6 +84,10 @@
|
|||
<div class="small">MeshCore live map • markers update in real time{{TRAIL_INFO_SUFFIX}} • click logo to hide/show HUD elements</div>
|
||||
<div class="small">{{SITE_FEED_NOTE}}</div>
|
||||
<div class="small" id="stats"></div>
|
||||
<div class="hud-update" id="update-banner" {{UPDATE_BANNER_HIDDEN}}>
|
||||
<span class="hud-update-text" id="update-text">Update available</span>
|
||||
<button class="map-toggle hud-update-dismiss" id="update-dismiss" type="button">Hide</button>
|
||||
</div>
|
||||
<div class="node-search">
|
||||
<input class="node-search-input" id="node-search" type="text" placeholder="Search nodes by name or key..." autocomplete="off" />
|
||||
<div class="node-search-results" id="node-search-results" hidden></div>
|
||||
|
|
@ -256,6 +270,6 @@
|
|||
></script>
|
||||
<script src="https://unpkg.com/leaflet.heat/dist/leaflet-heat.js" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="/static/app.js?v=peers1" defer></script>
|
||||
<script src="/static/app.js?v=peers2" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -434,4 +434,26 @@
|
|||
@keyframes route-dash {
|
||||
to { stroke-dashoffset: -200; }
|
||||
}
|
||||
|
||||
.hud-update {
|
||||
margin-top: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 12px;
|
||||
background: rgba(249, 115, 22, 0.12);
|
||||
border: 1px solid rgba(249, 115, 22, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #fed7aa;
|
||||
}
|
||||
|
||||
.hud-update-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hud-update-dismiss {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ services:
|
|||
SITE_URL: "${SITE_URL:-/}"
|
||||
SITE_ICON: "${SITE_ICON:-/static/logo.png}"
|
||||
SITE_FEED_NOTE: "${SITE_FEED_NOTE:-Feed: Boston MQTT.}"
|
||||
CUSTOM_LINK_URL: "${CUSTOM_LINK_URL:-}"
|
||||
GIT_CHECK_ENABLED: "${GIT_CHECK_ENABLED:-false}"
|
||||
GIT_CHECK_FETCH: "${GIT_CHECK_FETCH:-false}"
|
||||
GIT_CHECK_PATH: "${GIT_CHECK_PATH:-/app}"
|
||||
GIT_CHECK_INTERVAL_SECONDS: "${GIT_CHECK_INTERVAL_SECONDS:-43200}"
|
||||
DISTANCE_UNITS: "${DISTANCE_UNITS:-km}"
|
||||
NODE_MARKER_RADIUS: "${NODE_MARKER_RADIUS:-8}"
|
||||
MQTT_HOST: "${MQTT_HOST:-localhost}"
|
||||
|
|
@ -44,3 +49,4 @@ services:
|
|||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./:/repo:ro
|
||||
|
|
|
|||
12
docs.md
12
docs.md
|
|
@ -5,6 +5,10 @@ This document captures the state of the project and the key changes made so far,
|
|||
## Overview
|
||||
This project renders live MeshCore traffic on a Leaflet + OpenStreetMap map. A FastAPI backend subscribes to MQTT (WSS/TLS), decodes MeshCore packets using `@michaelhart/meshcore-decoder`, and broadcasts device updates and routes over WebSockets to the frontend. Core logic is split into config/state/decoder/LOS/history modules so changes are localized. The UI includes heatmap, LOS tools, map mode toggles, and a 24‑hour route history layer.
|
||||
|
||||
## Versioning
|
||||
- `VERSION.txt` holds the current version string.
|
||||
- `VERSIONS.md` is an append-only changelog by version.
|
||||
|
||||
## Key Paths
|
||||
- `backend/app.py`: FastAPI server + MQTT lifecycle and websocket broadcasting.
|
||||
- `backend/config.py`: environment/config constants (shared across backend modules).
|
||||
|
|
@ -29,6 +33,12 @@ This project renders live MeshCore traffic on a Leaflet + OpenStreetMap map. A F
|
|||
- `curl -s http://localhost:8080/debug/last` (recent MQTT decode/debug entries).
|
||||
- `curl -s http://localhost:8080/peers/<device_id>` (peer counts for a node; uses route history).
|
||||
|
||||
## Env Notes (Recent Additions)
|
||||
- `CUSTOM_LINK_URL` adds a HUD link button; blank hides it.
|
||||
- `MQTT_ONLINE_FORCE_NAMES` forces named nodes to show MQTT online and skips them in peers.
|
||||
- `GIT_CHECK_ENABLED`, `GIT_CHECK_FETCH`, `GIT_CHECK_PATH` enable update checks.
|
||||
- `GIT_CHECK_INTERVAL_SECONDS` controls how often the server re-checks for updates.
|
||||
|
||||
## MQTT + Decoder
|
||||
- MQTT is **WebSockets + TLS** (`MQTT_TRANSPORT=websockets`, `MQTT_TLS=true`, `MQTT_WS_PATH=/` or `/mqtt`).
|
||||
- Decoder uses Node + `@michaelhart/meshcore-decoder` installed in the container.
|
||||
|
|
@ -66,6 +76,8 @@ This project renders live MeshCore traffic on a Leaflet + OpenStreetMap map. A F
|
|||
- PWA install support is enabled via `/manifest.webmanifest` and a service worker at `/sw.js`.
|
||||
- Clicking the HUD logo hides/shows the left panel while tool panels stay open.
|
||||
- Share button copies a URL with the current view + toggles (including HUD visibility).
|
||||
- Optional custom HUD link appears when `CUSTOM_LINK_URL` is set.
|
||||
- Update banner shows when `GIT_CHECK_ENABLED=true` and the repo is behind; users can dismiss it per remote SHA.
|
||||
- URL params override stored settings: `lat`, `lon`/`lng`/`long`, `zoom`, `layer`, `history`, `heat`, `labels`, `nodes`, `legend`, `menu`, `units`, `history_filter`.
|
||||
- Service worker uses `no-store` for navigation requests so env-driven UI toggles (like the radius ring) update without clearing site data.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue