Commit Graph
912 Commits
Author SHA1 Message Date
pablorevilla-meshtastic f5d5dd6049 more updates 2026-10-29 13:10:17 -07:00
pablorevilla-meshtastic 8446ef8956 fixed colors in report 2026-10-29 10:47:51 -07:00
pablorevilla-meshtastic 7c8f5c42d0 fix one of the reports 2026-10-28 11:43:10 -07:00
pablorevilla-meshtastic 11b6a92f71 update 2026-10-28 11:26:11 -07:00
pablorevilla-meshtastic d022ec455e more updates 2026-10-28 11:21:57 -07:00
pablorevilla-meshtastic acba2284c5 more altitude 2026-10-28 11:10:52 -07:00
pablorevilla-meshtastic dddc8e0a13 more altitude details 2026-10-28 10:57:26 -07:00
pablorevilla-meshtastic 93fc31efd4 add altitude to table 2026-10-28 10:39:25 -07:00
pablorevilla-meshtastic 3e1d95ed2c fis 0 lat and long 2026-10-28 10:31:18 -07:00
pablorevilla-meshtastic ec9fafc362 change icon on light mode 2026-10-28 10:10:17 -07:00
pablorevilla-meshtastic 0e424da10a new stats report 2026-10-28 09:14:37 -07:00
pablorevilla-meshtastic 7ad590a83c update 2026-10-28 07:51:46 -07:00
pablorevilla-meshtastic b80a0b27df node page optimization 2026-10-28 07:49:04 -07:00
pablorevilla-meshtastic 330ee47932 updated version 2026-10-28 07:44:30 -07:00
pablorevilla-meshtastic 29860e8e7d fix report 2026-10-28 07:38:31 -07:00
pablorevilla-meshtastic ef52e96d7c fix date on neighour repor 2026-10-28 07:35:42 -07:00
pablorevilla-meshtastic acfd71bd2e fix 2nd key funtion 2026-10-27 19:50:39 -07:00
pablorevilla-meshtastic f570fdb65a Merge develop-4 UI updates into develop 2026-10-27 17:27:15 -07:00
11fb764fa8 Keep one malformed packet from breaking API endpoints or node updates (#176)
* Keep one malformed packet from breaking API endpoints or node updates

Values in a packet come from the mesh, so a forged packet or a newer firmware can carry
data the code did not expect:

- Routing.error_reason with a number the bundled protobufs do not define made
  Routing.Error.Name() raise, and /api/packets returned 500 for every list that included
  that packet.
- An unknown DeviceConfig.Role or HardwareModel in MAP_REPORT or NODEINFO raised the same
  way, and the whole node update was dropped.
- rx_snr of +inf was stored and served as "Infinity", which is not valid JSON, so the
  packet page's "seen by" list failed to load.
- A NEIGHBORINFO packet whose payload does not parse made /api/edges return 500 while it
  was among the 50 most recent.

Add decode_payload.enum_name(), which returns "unknown(N)" for undefined values, and use it
at all five enum lookups. Store non-finite SNR as NULL and serve existing ones as null.
Skip NEIGHBORINFO packets that do not decode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remove a blank line that fails the ruff format check

`ruff format --check .` (the Ruff workflow, ruff 0.13.3) fails on develop because of this
line, so every pull request's lint job is red regardless of its content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Nathan <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:20:08 -07:00
c0c9a499d5 Escape node names and channels in chart tooltips and parsed telemetry (#177)
Three places still put mesh-provided strings into HTML unescaped:

- stats.html: the pie-chart tooltip formatter returned params.name raw. The charts cover
  channel, hardware model, role and gateway firmware, so a channel or firmware string ran
  as HTML when someone hovered its slice.
- traceroute.html: the node tooltip formatter returned the long name raw.
- packet.html: "PDOP", "sats_in_view", "ground_speed" and "altitude" values parsed from
  the payload text went into the card unescaped. A broadcast text message reading
  "PDOP: <img ...>" is enough.

Use the existing escapeHtml() from static/html-escape.js in all three.

Co-authored-by: Nathan <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:19:16 -07:00
cfa71f9235 Keep one node's data from stopping the kiosk map or skewing the stats pies (#178)
/kiosk builds every marker in one forEach, so a single node with unexpected data stops
the whole page:

- node.role.toLowerCase() throws when role is null. Nodes first heard through a POSITION
  packet are created by ensure_node_exists() without a role, so on a fresh database the
  kiosk draws no nodes at all once one of them has a position.
- markers is a plain object keyed by channel name. A node on a channel called "__proto__",
  "constructor" or "toString" finds an existing non-array value there, and .push() throws.

Either error aborts the loop, and the channel filters and live packet blinking after it
never start. Treat a missing role as "not a router" and keep markers in a Map.

/stats counted values per field in a plain object too, so a channel called "constructor"
was counted as the string "function Object() { [native code] }1" and "__proto__" was lost.
Count in a Map there as well.

Co-authored-by: Nathan <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:18:36 -07:00
5f3d37bf06 Build every CSV export through one helper that quotes cells and defuses formulas (#179)
The four "Export CSV" buttons each built CSV by hand, from values that come from the mesh:

- /nodelist used a data: URL through encodeURI(), which leaves "#" alone, so the file
  ended at the first "#" in any cell. Real node names contain one ("Big Rock Ridge #1").
- The node page's chart export (neighbors, telemetry) did not quote cells at all.
- /stats quoted cells but did not double quotes inside them, so a channel such as
  Mesh "West", Bay split into extra columns.
- None of them guarded against spreadsheet formulas. A node name, channel, firmware string
  or message that starts with = + - or @ opens as a live formula in Excel or LibreOffice.

Add static/csv.js (loaded from base.html, like html-escape.js) with csvCell(), which
quotes every cell, doubles inner quotes and prefixes formula-like text with ', leaving
plain numbers alone, and csvFromRows(). All four exports use it; /nodelist now downloads a
Blob like the others.

Co-authored-by: Nathan <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:18:11 -07:00
1e9b1a3ce3 Answer 400, not 500, for malformed or out-of-range numeric parameters (#180)
* Answer 400, not 500, for malformed or out-of-range numeric parameters

Many endpoints turned query or path values into integers with a bare int(), so
?length=abc on /api/channels or /api/stats/top, /api/traceroute/abc and
/graph/traceroute/abc answered with an unhandled exception and a 500.

Values that do parse could still fail: node and packet ids are BIGINT and portnum is
INTEGER, and a longer number made the database driver raise, which also answered 500
(/api/packets?node_id=99999999999999999999999, /api/packets_seen/<same>, the node QR and
coverage routes, /api/stats and /api/stats/count filters). On PostgreSQL that includes
any portnum outside 32 bits.

length had no upper bound, so a huge value overflowed timedelta (500) or widened the
aggregate to the whole table. On SQLite a negative limit means "no limit", so
/api/stats/top?limit=-1 returned every node and skipped its 100-row cap.

Add parse_db_int(), which also rejects values outside the column's range, plus two small
helpers that answer 400 with a message. Clamp length to 1..3650, limit to 1..100 and
offset to >= 0. Endpoints that already ignored an unparseable filter (the /api/packets
node, portnum and time filters) treat an out-of-range one the same way. Valid requests are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remove a blank line that fails the ruff format check

`ruff format --check .` (the Ruff workflow, ruff 0.13.3) fails on develop because of this
line, so every pull request's lint job is red regardless of its content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Nathan <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:16:50 -07:00
d66c5d21e9 Log request values and MQTT topics with %r so they cannot add log lines (#181)
* Log request values and MQTT topics with %r so they cannot add log lines

/api/packets logged an unparseable since, portnum or node id filter with an f-string, so
the decoded query value went into the log as is. ?since=1%0A<fake line> wrote a second,
forged log line, and escape sequences (ESC, BEL) reached whoever reads the log in a
terminal. The ingest error added in startdb.py logged the MQTT topic with %s.

Use %r for these values, as the gateway_id warning in mqtt_store.py already does, so CR,
LF, ESC and other control characters appear escaped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remove a blank line that fails the ruff format check

`ruff format --check .` (the Ruff workflow, ruff 0.13.3) fails on develop because of this
line, so every pull request's lint job is red regardless of its content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Nathan <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:15:33 -07:00
a5f46d6360 Bound the traceroute data a request has to draw (#182)
* Bound the traceroute data a request has to draw

Every TRACEROUTE message is stored as a new row under its packet id, and its route can list
any number of hops. /graph/traceroute decodes every row, looks up every node on every route
and runs Graphviz over the result inside the request handler, so a publisher can make one
page view take a very long time and stall every other request meanwhile:

- 3,000 rows of 8 random hops for one packet id: the graph took about 43 s and other
  requests waited up to 24 s.
- 150 rows of 400 hops: about 40 s, then a 500, with other requests waiting about 10 s.

Real traceroutes are small. In 17 days of bay-area traffic no packet id had more than 72
rows, and none of 66,709 routes had more than 8 hops in either direction, which is the
firmware's limit.

get_traceroute() now returns at most the 200 earliest rows, and the TRACEROUTE decoder
treats a route longer than 8 hops as undecodable, which every caller already skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remove a blank line that fails the ruff format check

`ruff format --check .` (the Ruff workflow, ruff 0.13.3) fails on develop because of this
line, so every pull request's lint job is red regardless of its content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Nathan <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:10:03 -07:00
Mike BennerandClaude Opus 5.5 ae7ba15263 Include the node's public key in the contact QR code (#185)
* Include the node's public key in the contact QR code #184

The contact QR code and import URL never set user.public_key. Current
Meshtastic apps refuse to import a keyless contact. On 2.7.x firmware,
an older app's add_contact also overwrites the key the radio already
holds, so direct messages to that node fail.

Read the key from the node's latest stored NODEINFO_APP packet. Share
the contact only when a 32-byte key is known. Otherwise the node page
hides the QR buttons and says why. The QR API response now also
returns public_key.

Also drop the stray blank line that fails ruff format --check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Do not share a contact when a node has reported more than one key #184

A forged NODEINFO uplinked over MQTT could otherwise become the newest
packet, and the shared contact would then replace the key a scanning
radio already trusts. The QR endpoint now reads every stored NODEINFO
from the node and refuses to share when their 32-byte keys disagree.
It reports that in public_key_conflict, and the node page says why.

Add an index on packet (from_node_id, portnum, import_time_us DESC) so
the lookup reads only the node's NODEINFO packets. Order with NULLS
LAST so PostgreSQL does not treat old rows without import_time_us as
newest. Carry is_licensed and is_unmessagable into the contact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Skip NODEINFO rows with no payload when building the contact #184

packet.payload is nullable, and MeshPacket.FromString(None) raises
TypeError, so one such row made the QR endpoint answer 500.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:08:54 -07:00
pablorevilla-meshtastic b87e9b9917 mode changes 2026-10-26 21:55:56 -07:00
pablorevilla-meshtastic 0352ac5802 more cosmetic changes 2026-10-26 16:37:17 -07:00
pablorevilla-meshtastic daeed58042 latest updates 2026-10-26 15:17:14 -07:00
pablorevilla-meshtastic f936b32a1c add node list to map 2026-10-26 10:03:14 -07:00
pablorevilla-meshtastic 92f5b16073 more updates 2026-10-26 09:51:41 -07:00
pablorevilla-meshtastic 03ca33143b move visual changes 2026-10-26 09:45:28 -07:00
pablorevilla-meshtastic de81155e9b more cosmetic changes 2026-10-26 09:27:03 -07:00
pablorevilla-meshtastic ec67254472 quick map fixes 2026-10-26 09:21:10 -07:00
pablorevilla-meshtastic ef926798fe chat colors changes 2026-10-25 22:25:15 -07:00
pablorevilla-meshtastic 278d3b7e94 First push lots of changes 2026-10-25 22:24:48 -07:00
pablorevilla-meshtastic aabb099468 update protobufs 2026-10-24 11:15:56 -07:00
pablorevilla-meshtastic 92900362dc change readme 2026-10-23 22:27:35 -07:00
pablorevilla-meshtastic df7766e783 change reporing on Snapshot 2026-10-19 17:37:30 -07:00
pablorevilla-meshtastic fb3a0154dd some updates 2026-10-15 22:49:55 -07:00
pablorevilla-meshtastic 012150cba9 update big 2026-10-14 10:12:11 -07:00
pablorevilla-meshtastic ef37a910b9 change packet reach activity 2026-10-14 10:00:17 -07:00
pablorevilla-meshtastic f4fbf16e4f uncheck packets reach 2026-10-14 08:46:00 -07:00
pablorevilla-meshtastic f9b684ee44 fix template 2026-10-14 07:36:27 -07:00
cd78c392e9 Bound /api/coverage parameters and run the ITM model off the event loop (#172)
* 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>
2026-10-14 07:26:18 -07:00
96b49bb893 Keep the MQTT writer running when a message is malformed (#171)
* 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>
2026-10-14 07:26:03 -07:00
5900c41cf4 Escape mesh-provided strings before inserting them into the page (#170)
* 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>
2026-10-14 07:25:01 -07:00
5214b233c1 Keep /api/lang and the static page route inside their directories (#173)
* 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>
2026-10-14 07:22:37 -07:00
0109d68c1b Do not let payload search match redacted direct messages (#174)
* 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>
2026-10-14 07:21:05 -07:00
81612128a5 fix: limit the neighbor SNR chart to the last 7 days (#165)
* 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>
2026-09-18 10:37:37 -07:00