* Update CONTRIBUTING.md
* Update CONTRIBUTING.md
* Bound /api/coverage parameters and run the ITM model off the event loop
The coverage endpoint took radius_km, step_km and the other model inputs
straight from the query string with only a float() conversion. The ITM
loop advances by step_km until it passes radius_km, so step_km=0 (or a
negative value) never ends and radius_km=inf never ends either; a very
small step with a large radius means millions of model calls. All of this
ran synchronously inside the request handler, so one such request stalls
every other request the server is handling.
- Reject non-finite values and anything outside a sane range for each
parameter (400 with a message saying the allowed range).
- Cap radius_km / step_km at 4000 steps.
- Run compute_perimeter / compute_coverage in a worker thread so the event
loop keeps serving other requests while the model runs.
- Make the loops themselves refuse a non-positive step or non-finite radius,
so a future caller cannot reintroduce the hang, and shrink the result
cache (64 entries) since bounded inputs make large results possible.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Pablo Revilla <pablorevilla@gmail.com>
Co-authored-by: Nathan <nathan@stonekitty.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
* Update CONTRIBUTING.md
* Update CONTRIBUTING.md
* Keep the MQTT writer running when a message is malformed
startdb.py stores every message it receives with mqtt_store.process_envelope,
and nothing caught an exception from it. The gateway id in a ServiceEnvelope
was converted with int(gateway_id[1:], 16) without checks, so a gateway id
of "x", "!zz" or a very long hex string raised ValueError/OverflowError,
the ingest task died and startdb.py exited. A NODEINFO whose id spelled an
existing node differently ("!00abcd" vs "!abcd") hit the UNIQUE constraint
on node.node_id at commit time with the same result. Anyone who can publish
to the broker can trigger these, and repeating them keeps the writer in a
restart loop so the site stops receiving packets.
- Parse gateway and user ids with one helper that returns None for anything
that is not "!" + hex (or does not fit a 64-bit column) and skip the
PacketSeen/Traceroute/Node update instead of raising.
- When a NODEINFO id is new but its node_id already exists, update that row
instead of inserting a duplicate.
- Wrap the per-message store call in startdb.py so an unexpected error is
logged and the loop moves on to the next message.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Pablo Revilla <pablorevilla@gmail.com>
Co-authored-by: Nathan <nathan@stonekitty.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
* Update CONTRIBUTING.md
* Update CONTRIBUTING.md
* Escape mesh-provided strings before inserting them into the page
Node names, firmware strings, channel ids and message text all come from
the radio network, so anyone with a Meshtastic node (or publish access to
the MQTT broker) chooses their contents. Several pages dropped those
values straight into innerHTML / insertAdjacentHTML / Leaflet popups,
which lets a crafted node name run JavaScript in every visitor's browser.
Add a small shared helper (static/html-escape.js, loaded by base.html and
kiosk.html) and route every mesh-controlled value on the node list, top
nodes, firehose, node, packet and kiosk pages through it. Pages that
already had a local escapeHtml (chat, net, map, reliability) are unchanged.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Pablo Revilla <pablorevilla@gmail.com>
Co-authored-by: Nathan <nathan@stonekitty.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
* Update CONTRIBUTING.md
* Update CONTRIBUTING.md
* Keep /api/lang and the static page route inside their directories
Two routes built a filename from request input without checking where it
ended up:
- /api/lang?lang=... joined the value onto the lang directory and added
".json". os.path.join discards the directory when the value is an
absolute path, so ?lang=/some/where/file returned /some/where/file.json.
Any .json file the server process can read was served.
- /{page} appended ".html" and read static/<page>. aiohttp passes an
encoded slash (%2F) through a single path segment and unquotes it, so
/..%2F..%2F..%2Fx read ../../../x.html relative to the static directory.
The lang route now only accepts a plain language code (en, es, pt-BR, ...)
and falls back to English otherwise, exactly as it already did for unknown
languages. The page route resolves the final path and requires it to be a
file inside meshview/static, the same check /docs/{doc} already does.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Pablo Revilla <pablorevilla@gmail.com>
Co-authored-by: Nathan <nathan@stonekitty.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
* Update CONTRIBUTING.md
* Update CONTRIBUTING.md
* Do not let payload search match redacted direct messages
/api/packets shows the text of a direct message (TEXT_MESSAGE_APP to a
specific node) as "<redacted>". The contains= filter, however, is a LIKE
over the stored packet bytes, which include the decrypted text for
messages on a known channel key. With portnum left out of the request,
contains=<guess> returned the packet when the guess appeared in a redacted
message, so the text could be recovered one substring at a time.
Direct text messages are now excluded from the substring match. Broadcast
messages and all other packet types search exactly as before.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Pablo Revilla <pablorevilla@gmail.com>
Co-authored-by: Nathan <nathan@stonekitty.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
* Update CONTRIBUTING.md
* Update CONTRIBUTING.md
* fix: limit the neighbor SNR chart to the last 7 days
The chart asked for the last 500 neighbor-info packets and put no
limit on how old they could be. On a busy node that is several weeks
of history, and every neighbor heard in that span gets its own line.
On one node it was 119 packets covering 21.8 days and 339 lines.
The chart now asks for the last 7 days, using the `since` parameter
that /api/packets already supports, and draws only the 15 neighbors
with the most data points in that window. A subtitle says how many
neighbors are shown and how many there were, so a missing line is not
a mystery.
Three smaller fixes in the same function:
- Neighbor names are looked up only for the lines that are drawn.
fetchNodeFromApi() does not cache a node it cannot find, so every
unknown neighbor was fetched again on every packet it appeared in.
- Each point is read from a Map instead of calling indexOf() on the
timestamp list, which was quadratic in the number of points.
- Two neighbors sharing a short name are told apart. ECharts
identifies a series by its name, so they were drawn as one line.
The "Export CSV" button for this chart wrote an empty file, because
neighborData was declared but never filled in. It is now filled in
from the neighbors on screen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Pablo Revilla <pablorevilla@gmail.com>
Co-authored-by: Nathan <nathan@stonekitty.net>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fallback palette used for channels that are not Meshtastic presets was
built out of pastels, which are unreadable against the light OSM tiles. Six
of the nine entries scored under 1.6:1 contrast on the map: #fffac8 at
1.06:1 and #aaffc3 at 1.18:1 were effectively invisible, leaving only the
white marker outline showing.
The underlying issue is that one color feeds two opposite backgrounds. The
marker fill sits on light map tiles, while the filter label is text on the
dark Bootstrap panel. The old palette was calibrated only for the panel.
Replace it with 12 colors chosen by a search over HSL, constrained to clear
3:1 contrast against both backgrounds, stay at deltaE >= 30 from every
channelColorOverrides preset, and deltaE >= 20.9 from each other. Saturation
is capped at 80% to keep out neon tones that would collide with ShortTurbo.
Growing the palette from 9 to 12 entries is safe: the index is derived from
palette.length rather than a fixed number.
Meshtastic stores well-known channel PSKs as a single byte index, so a
channel configured with "Ag==" was base64-decoded to one byte and handed
straight to AES. That is not just a failure to decode: algorithms.AES()
raises ValueError on a 1 byte key, and get_topic_envelopes only catches
MqttError, so the reader died on the first packet.
Expand single byte PSKs the way the firmware does in Channels::getKey --
copy the default PSK and bump its last byte by index - 1, so index 1 is
the default PSK itself. Index 0 means no encryption and is skipped.
Also validate that decoded keys are 16 or 32 bytes and log the offending
config value instead of letting a bad key reach AES.
* fix: correct traceroute path orientation when building graph edges
A completed traceroute is observed as a response packet, whose from/to are
reversed with respect to the path being traced: `to` is the node that started
the traceroute and `from` is the node that answered. Meshview assembled paths
as [from, *route, to], so both endpoint-adjacent edges were wrong for every
completed traceroute with at least one intermediate hop. Because the false
edges always attach to the endpoints, a busy initiator or gateway accumulated
false direct links to every remote target it ever traced.
Confirmed against TraceRouteModule::alterReceivedProtobuf, which renders a
response as printRoute(r, p.to, p.from, false), against the meshtastic python
CLI, which starts the forward path at p["to"] and the return path at p["from"],
and against real recorded traceroutes where a request's route[] is a prefix of
the matching response's route[].
Add meshview/traceroute.py and use it from /api/edges, /api/traceroute and
graph_traceroute, which had three separate copies of this logic that had all
drifted. /api/traceroute also gains initiator/target so the traceroute page
stops re-deriving the endpoints itself, which is where a fourth copy lived.
While in /api/edges, skip the NODENUM_BROADCAST placeholders that
insertUnknownHops() writes into route[] (they became a pseudo-node hub in the
graph) and guard against the (gateway, gateway) self edge that
graph_traceroute already guarded against.
* fix: do not invent links across unknown hops or unfinished returns
Review of becf4a8 found two remaining path errors.
Dropping NODENUM_BROADCAST from route[] during assembly joined the nodes
either side of it, so A -> B -> ? -> C -> D became A -> B -> C -> D and a
lone unknown hop collapsed into a false direct A -> D. Keep the placeholder
in the path and skip the edges that touch it instead. On a 2660 row sample
this removes 10 links that do not exist.
done=True only means a response was observed, not that it arrived. A gateway
usually reports one still travelling home with route_back holding just the
hops so far, so always appending the initiator claimed a final link that was
never seen. Append it only when the response reached the initiator, which the
spare snr_back entry from SNRonly=isToUs() marks. Note this is rare in
practice: 2 of 303 real responses, since the entry is added by the initiator
and gateways see packets in flight.
The deduplicated unique_forward_paths and unique_reverse_paths carry no
per-observation provenance, so the traceroute page can no longer tell which
of them reached the far endpoint. It now adds only the adjacent endpoint.
* fix: treat a response as proof the forward path reached the target
Winning path selection required return hops, so a response observed before
its first return hop contributed nothing and the page fell back to the
deduplicated reported paths, which cannot tell which of them reached the
target. A response with route=[B,C] and no route_back rendered A > B > C,
losing the target and the last link.
Select winning paths from response observations instead. A response only
exists because the request arrived, whatever its return trip went on to do.
Dropping the non-empty forward_hops guard at the same time means a zero hop
response contributes its real direct link, so a fully direct traceroute and a
completed direct return are no longer excluded from the graph.
Mark reported forward paths that some response reported, so the page can
close those on the target and they deduplicate against the winning path
instead of drawing a shorter copy underneath it.