* fix(web): escape untrusted mesh fields in dashboard to close DOM XSS
Security review found user-controlled mesh data reaching innerHTML unescaped
(upstream-originating; worth a PR back to l5yth/potato-mesh):
- High: channel_name was interpolated raw by formatChatChannelTag and rendered
via innerHTML on the node-detail page and map overlays, so a crafted channel
name (any radio can set one) executed script for every viewer. Now escaped
with the canonical escapeHtml.
- Defense-in-depth: node_id, role, and hw_model in the node table are now
escaped for consistency with sibling cells.
Regression test added for the channel-name escape. Full JS suite: 1448 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(web): gate POST /api/instances by federation mode + hardening
Security review findings in the Sinatra app (upstream-originating; worth a PR
back to l5yth/potato-mesh):
- High: POST /api/instances performed outbound federation fetches and DB writes
with no federation/private guard, so a PRIVATE=1 or FEDERATION=0 node still
acted on unsolicited signed announcements. Added `halt 404 unless
federation_enabled?`, mirroring the existing GET /api/instances guard.
- Low: the JSON-LD <script> block now escapes </script> breakout characters.
- Low: federation peer fetches now enforce a max response size
(REMOTE_INSTANCE_MAX_RESPONSE_BYTES, default 8 MiB) to bound memory.
Regression spec added for the federation-gate (private + FEDERATION=0 cases).
NOTE: rspec could not be run locally (this host has only Ruby 2.6; the project
needs >=3.0 and no Docker was available). Changes are `ruby -c` syntax-checked
and reviewed against the mirrored guard; the ruby.yml CI workflow must confirm
the spec on push/PR before merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(matrix): stop silent message loss + add HTTP timeouts + log reg failures
Security/resilience review of the Matrix bridge (upstream-originating; worth a
PR back to l5yth/potato-mesh):
- High: within a poll batch, a later message that forwarded successfully
advanced the in-memory rx_time watermark past an EARLIER message that failed,
so the failed message was never refetched -> silent permanent loss. poll_once
now breaks at the first failure, committing only the contiguous successful
prefix; the failed message and everything after it are retried in order next
poll (no reordering, no duplicates). Regression test added and proven to bite.
- High: the shared reqwest client had no timeouts, so a hung homeserver/API
stalled the single-threaded poll loop (and startup health checks) forever.
Added timeout(30s) + connect_timeout(10s).
- Medium: ensure_user_registered swallowed all non-success responses silently;
it now warns (status + body) on anything other than the expected
already-registered case, so a bad as_token is diagnosable.
cargo test: 118 passed; cargo fmt clean. Tradeoff: a permanently-failing
message now blocks its batch (retried each poll) rather than being lost -- safer
than silent loss; a skip-after-N/dead-letter follow-up is possible if needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: address Copilot review on the security PR (Matrix errcode + spec hygiene)
- matrix.rs: ensure_user_registered now treats only the specific M_USER_IN_USE
errcode as "already registered", instead of any 400 Bad Request, so real 400
failures (malformed request, config issues) reach the diagnostic warn branch.
Updated the M_USER_IN_USE test to send the errcode body and added a test that
a non-M_USER_IN_USE 400 is not suppressed. cargo test: 119 passing.
- app_spec.rb: the private-mode POST /api/instances example now restores
ENV["PRIVATE"] via an around/ensure block (mirroring the FEDERATION pattern),
so private mode can't leak into later specs and cause order-dependent failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: implement Copilot follow-ups — streaming size cap + bounded skip
- instance_fetcher.rb: perform_single_http_request now uses the block form of
Net::HTTP#request + response.read_body to enforce the response-size cap
INCREMENTALLY, aborting as soon as the limit is exceeded, instead of letting
the non-block form buffer the whole body into memory first. Updated the two
federation_spec mocks to the block/streaming form and added a size-cap test.
- main.rs (Matrix bridge): a permanently-failing ("poison") message no longer
blocks the batch forever. Consecutive per-message failures are counted
in-memory; after MAX_FORWARD_ATTEMPTS (5) the message is skipped (advanced
past, with a warning) so later messages make progress. Transient failures
still stop-and-retry in order (no silent loss, no reordering). Added a
skip-after-N regression test; the original watermark test still passes.
cargo test: 120 passing, cargo fmt clean.
Ruby: instance_fetcher.rb is `ruby -c` clean; the spec exercises the new paths
but rspec/`ruby -c` can't run locally (Ruby 2.6 here can't parse the repo's
3.1+ syntax) — the ruby.yml CI is the gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(web): fix private-mode /api/instances spec to actually enable private mode
Running the suite locally (Ruby 4.0) revealed the private-mode POST /api/instances
example never entered private mode: the top-level `before` hook deletes
ENV["PRIVATE"] before each example, so toggling it (even via an around block)
had no effect and the request returned 201 instead of 404.
Switch to stubbing PotatoMesh::Config.private_mode_enabled? => true, the pattern
the rest of the suite uses for private-mode cases. This makes the test actually
exercise the guard AND removes the ENV manipulation entirely (so there is no
cross-spec ENV leak to worry about). Full web suite: 1470 examples, 0 failures;
rufo clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* web: initial-load module-graph waterfall
* web: prefetch initial API data on cold load (faster first paint)
Second phase of the initial-load fix (after the module-graph preload in
8915f8c). Even with the graph preloaded, the first /api/* fetch waited for the
~806KB bundle to download, parse, and boot. An early <script type="module"
async> boot module (main/boot-prefetch.js) now fires the first-load (since=0)
requests in parallel with the module graph (at priority:'high') and stashes the
in-flight Response promises on window.__PM_BOOT__; refresh() consumes them on its
first cold refresh via a new responsePromise option on the data-fetchers instead
of issuing its own requests.
Cold loads only: a synchronous localStorage marker (pm:cache-present, maintained
by the cache write-back / clear / disable paths) suppresses the prefetch on warm
revisits, leaving the FC2 seed-then-delta path untouched. Message endpoints are
skipped in private mode (data-pm-chat="false"), mirroring the /api/messages 404
(Invariant II / PS6). Pure pre-warm: an absent or rejected prefetch re-fetches,
so it is never load-bearing (FC7).
Read-side only; no API/DB/ingestor change, no new dependency. Adds ACCEPTANCE
EF-A1/EF-A2/EF-R1; 100% line/branch/func coverage on the new module (lcov);
rspec + npm test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: add guardrails
* docs: fix meshcore badge
* web: life chat message cap at 1000 in frontend
* web: honor protocol in chat
* web: deprioritize test channels
* fix ci
* Fix regression where Meshcore chat senders show as Meshtastic
* Address review feedback for protocol misclassification fix
- ingest.rb: exclude wrapper ``protocol`` key from /api/nodes batch-limit
count so the documented 1000-node maximum still applies after the
Python ingestor started stamping protocol at the wrapper level.
- Drop plan-file references from production and test comments per the
repo guidelines; the why is already explained inline.
* Address protocol-fallback review feedback
- Neighbor placeholder now inherits the source node's protocol from the
surrounding /api/neighbors entry, so the badge tracks the radio the
peer lives on instead of collapsing to the neutral "Unknown" label
(review item #1).
- resolve_record_protocol logs one warn_log line when an explicit
protocol stamp is rejected as malformed, making misbehaving custom
protocol adapters visible in the operator log instead of silently
falling back (review item #3).
* Extract buildNodePlaceholder helper for testability
The neighbor placeholder logic in main.js lives inside an untested
closure, so codecov reported the protocol-propagation lines as
uncovered. Extract the small placeholder builder into long-link-router
so it can be unit tested directly; the closure-internal call site stays
trivial (one factory call + one fallback call).
* web: refactor 6/7 node page
* web: address node-page refactor review and close coverage gaps
Fix the concurrency cap in fetchNodeDetailsIntoIndex so it actually
limits in-flight requests. The previous implementation built each
fetch as an immediately-invoked async IIFE, so all N fetches started
the moment the loop ran; the slicing-then-Promise.all step only
changed when settlement was observed, not when work began. Replace
the IIFE-then-batch pattern with a worker pool: a fixed-size set of
worker promises iterates a shared queue and only pulls the next
identifier once the previous fetch settles.
Reduce cross-module coupling around the role-aware short-name badge
by extracting renderRoleAwareBadge into a new badge.js module that
single-node-table, messages, detail-html, and traces import directly,
so the neighbour module is no longer pulled in by four non-neighbour
callers. Tighten applyDetails in role-index.js by hoisting the
ternary into a single key binding and dropping the redundant
instanceof Map guard.
Close the patch-coverage gap reported by Codecov: add tests for
parameter-validation paths in bootstrap (parseReferencePayload,
normalizeNodeReference, fetchNodeDetailHtml, initializeNodeDetailPage),
the worker-pool branches in role-index (no-fetch, empty queue, 404,
non-success responses, and an explicit concurrency-cap assertion),
the badge fallback path, the nested-neighbor seedNeighborRoleIndex
branches, the renderNeighborBadge metadata-merge and short-name
fallback paths, the empty-trace and empty-chart short-circuits, and
single-node-table validation. All ten node-page submodules now
report 100% line coverage.
* web: refactor 7/7 main js
* web: refactor 7/7 main js
* web: address review feedback on 7/7 main.js refactor
* Consolidate the duplicate ./main/format-utils.js import block in
main.js so all symbols come from a single, alphabetised import
statement (review item: "Important — Duplicate format-utils.js
import block").
* Replace the leftover stale JSDoc atop +createOfflineTileLayer+ with
one clear "do not inline" DI block, and likewise expand the
+fetchMessages+ wrapper docstring so future readers see the shim's
purpose without hunting for the implementation (review nit:
"thin wrappers ... worth a one-line JSDoc").
* Add per-module unit tests under
public/assets/js/app/main/__tests__/ covering every previously-
uncovered branch in the 9 modules codecov flagged: tile-coords,
sort-comparators, fullscreen-helpers, format-utils, data-fetchers,
data-merge, tooltip-html, long-link-router, and offline-tile-layer.
This drives the codecov patch percentage on PR #778 from 78.99%
to ~100% on the new modules and unblocks the codecov/patch gate.
JS suite: 1,114 tests, 0 failures.
* web: refactor 5/7 node page charts
* web: address review feedback on node-page-charts split
* Drop the local stringOrNull/numberOrNull copies from node-page.js
and import them from ./value-helpers.js so the shared module's
stated dedup actually happens (review issue #1). The two locals
were byte-identical to the new shared module.
* Split the display-only formatters out of
node-page-charts/format-utils.js into a sibling
node-page-charts/display-formatters.js so format-utils.js carries
only chart concerns (review issue #2). The barrel
node-page-charts.js re-exports both files so existing callers and
tests keep working unchanged.
* Inline +fmtCurrent+ in node-page-charts/specs.js and drop the
sideways import from short-info-telemetry.js so node-page-charts/
no longer depends on an unrelated module (review issue #3).
* Add a dedicated value-helpers.test.js pinning the contract of
+numberOrNull+ and +stringOrNull+ so they stop relying on
transitive coverage from the chart suite (review issue #5).
* web: refactor 2/7 federation
* web: close federation coverage gaps and apply review nits
Address Codecov patch coverage feedback by adding rspec examples for
the 51 lines flagged across the new federation shards (announce,
crawl, validation, http_client, self_instance, instance_metrics,
announcer_threads, lifecycle, signature). Per-shard line coverage in
the federation directory is now 100%.
Apply two review-comment changes: rename the awkwardly-named
http_client_get.rb to instance_fetcher.rb (matching its semantic
role rather than the HTTP verb), and declare PotatoMesh::App::Federation
explicitly in the federation.rb manifest so the namespace is owned by
this file rather than implicitly created by whichever shard happens to
load first.
* web: refactor 1/7 data processing
* web: close coverage gaps in data_processing submodules
Bring every file under lib/potato_mesh/application/data_processing/ to
100% line coverage so codecov/patch passes on the 1/7 refactor PR. The
gap was a relocation of pre-existing untested branches; closing them
here keeps the subsequent refactor PRs in the series unblocked.
* Add unit tests covering canonical sender/recipient overrides,
reply_id/emoji updates on existing rows, and the rare INSERT
ConstraintException recovery path inside +insert_message+.
* Cover the non-canonical reporter and per-neighbour resolution
branches in +insert_neighbors+.
* Cover the SQLException rescue in +upsert_ingestor+, the
fallback_num branch in +touch_node_last_seen+, the limit fallback
in +read_json_body+, the unrecognised-type branch in
+store_decrypted_payload+, the +power+ telemetry_type fallback,
the default-coercion path in +resolve_numeric_metric+, and the
numeric/bare-hex paths in +canonical_node_parts+ and
+coerce_trace_node_id+.
Drop dead code surfaced while pinning behaviour:
* +clear_encrypted+ in +insert_message+ has been initialised to
+false+ and never reassigned since #633 dropped the
decrypted-text override; remove it and the four dependent
branches.
* The +rescue ArgumentError; nil+ tails in
+identity.resolve_node_num+ and +traces.coerce_trace_node_id+ are
unreachable because every +Integer(...)+ call inside is guarded by
a regex pre-check.
Add a comment to the +data_processing.rb+ shim explaining that the
+require_relative+ list is ordered by dependency rather than
alphabetically, addressing review nit #5.
Backfill v0.6.1 CHANGELOG entry (previously undocumented),
add v0.6.2 entry covering 9 commits since v0.6.1, and bump
the version string across data, web, matrix, and app along
with the README docker-pull examples.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>