Files
meshcore-analyzer/docs/release-notes/v3.10.1.md
T
efitenandClaude Opus 5 cb6d230573 docs: renumber the release to v3.10.1 (#1953)
v3.10.0 was tagged and then withdrawn. **Nothing was ever available
under that number**: no container image and no release asset was ever
published, so no user could have pulled it.

This renames the notes and the CHANGELOG section. No product code
changes.

## Why it had to be renumbered

Three things, in the order they bit.

**1. The image never built.** `release-fast-path.yml` re-tags `:edge` to
`:vX.Y.Z` when the `:edge` revision label matches the tagged commit, and
dispatches `deploy.yml` when it does not. The tagged commit was
documentation-only, so the `paths-ignore` from #1949 meant no `:edge`
existed for it and the fallback ran. That part behaved correctly. The
fallback then published nothing, because every GHCR step was gated on
`github.event_name == 'push'` and a dispatch is not a push. It built
locally, reported `success`, and pushed nothing.

Fixed in #1951, but that fix is not in the `v3.10.0` tag, and a
`workflow_dispatch` runs the workflow file **from the ref it targets**.
So the existing tag could not be made to publish.

**2. The assets never uploaded.** I created the GitHub release by hand
before the workflow reached it, and `action-gh-release` cannot update an
immutable release. The correct procedure is to push the tag and let the
workflow create the release.

**3. The tag name cannot be reused.** GitHub's immutable releases keep a
tag name reserved even after the release is deleted:

```
remote: - Cannot create ref due to creations being restricted.
```

I established that only after deleting the release, which is the wrong
order. The lesson, written into the commit message so it survives: check
whether a tag can be rewritten before removing anything that depends on
it.

## What is in v3.10.1

The same 111 commits, plus the three CI fixes that landed after the
v3.10.0 tag (#1949, #1950, #1951). Those are listed in their own section
in the notes. **No product code differs** from what was tagged as
v3.10.0.

All 69 SHA references in the notes were re-verified after the rename.

## Procedure for this tag

Push the tag and stop. The workflow creates the release and attaches the
assets. Do not create it by hand.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 09:46:06 +02:00

12 KiB

CoreScope v3.10.1

v3.10.0 was tagged and then withdrawn before any container image or release asset was published, so nothing was ever available under that number. The release machinery could not publish an image for it (see #1951), and GitHub's immutable releases keep a tag name reserved once used, so this is the same release under the next number. Contents are identical apart from three CI fixes; no product code differs.

Upgrade urgency: High for anyone running a map. CARTO began requiring an API key on its raster basemaps in August 2026, so map tiles on v3.9.2 come back watermarked "API KEY REQUIRED" with an HTTP 200. Nothing errors and no healthcheck fires. This release supports the key; you have to add one. It also restores relay last_seen, which has been a no-op since the read-only DB refactor.

111 commits since v3.9.2 (2026-06-13), plus the CI fixes listed at the end. No auto-generated coverage bumps in this range, so all 111 are substantive. Every bullet ends with a commit SHA: git show <sha> to verify.

Highlights

  • Map tiles work again, once you add a CARTO key. Since 2026-08 CARTO answers unauthenticated tile requests with a watermarked image and HTTP 200, so this failed silently on every instance. map.tiles.providers.carto.key is now applied on every map surface rather than only the main map. Free keys need no account and no card. Verify by looking at a tile, not at a status code. (#1919, 7aa60c03; #1926, 8ce5291b)
  • Relay last_seen is written again. The server-side touch had been a no-op since the mode=ro refactor, so a repeater's last_seen had quietly degraded into "when did it last advertise". The write moved to the ingestor. Measured on a live instance: for repeaters that relayed within the last hour, the gap between last_relayed and last_seen drops from a median of 12,062 s to 193 s, and the share more than 5 minutes behind falls from 96% to 39%. (#1854, 6528c7ba)
  • RF telemetry from mobile clients, opt-in. Two independent streams: full-packet RF observations, and RF environment samples (noise floor, RX/TX airtime, CRC errors, packet counters) paired with a GPS track. Both default OFF, each independently gated, each with its own retention window. (#1905, 9e13e0b0; #1906, 9ae33874)
  • A path-trust threshold for hop attribution. A 1-byte hop prefix has only 256 possible values, which is weak evidence on a large mesh. pathTrust.minHashBytesForMapping now gates neighbour-edge creation and every display consumer. It ships at default 1, which is exactly the existing behaviour: raising it is an explicit operator decision, not something an upgrade does to you. (#1841, 8c48f2b4; #1863, e8f32df4; #1929, 176bb533)
  • The E2E suite tells the truth again. Three separate "flaky tests" turned out to be real product bugs, each one something operable before its own setup had finished: the Live view toggles are inert for about 100 ms after paint, the colour picker's deferred focus undid arrow-key navigation so Enter assigned the wrong colour, and an analytics theme-refresh silently discarded the filter you had just applied. All three are fixed, each with a regression test that fails on the previous commit. (#1940, ab62e86d; #1945, 5d2e14ab; #1944, e2df9bbd)

What is new

Mobile client RF telemetry (opt-in, default OFF)

  • Full-packet RF observations from mobile clients, stored in client_rx_observations. (#1905, 9e13e0b0)
  • RF environment samples on meshcore/client/<pubkey>/rf, stored in client_rf_samples: noise floor, RX and TX airtime, CRC errors and packet totals, paired with a GPS point. Absolutes only; deltas are derived at query time so a lost sample costs one interval instead of corrupting a running total. (#1906, 9ae33874)
  • Crowdsourced client-RX coverage, plus /api/nodes/resolve. (#1728, 22fe929d)

Map

  • Important Links overlay. (#1771, 0fd22039)
  • Topographic map layers. (#1891, 34b41fd5)
  • The Esri labels overlay now renders the labels it was already named for. (#1917, 25090230)

Nodes and export

  • Export the visible node list as MeshCore companion contacts JSON. (#1889, aabbd50d)
  • flood_advert_count_7d on the node detail endpoint. (#1831, d2ef624c)
  • unscoped_relay_count_24h per node. (#1823, bd0a58e1)
  • Transported region scopes in the repeater sidebar. (#1752, fc26fb6b)
  • Transport region scope on Packets: a detail-pane row and a sortable Scope column. (#1894, 376c3e9f)

Analytics

  • "My Repeaters" favourites monitoring dashboard. (#1761, 4654ce33)
  • Repeater metric scatter tab. (#1760, 7402e8d9)
  • The four-axis repeater usefulness score from #672 is complete. (#1762, 3efa37c4)
  • Per-node usefulness metrics documented in OpenAPI. (#1769, 17654dd0)

Live and packets

  • Optional "Multibyte only" view filter. (#1780, 5c0de8fb)
  • payload.destHash and payload.srcHash in filter autocomplete. (#1774, b3b8bec5)
  • A routed_through filter, clearer path semantics, and a fixed hex lexer error. (#1800, 5e096147)
  • Group Data (payload type 6) added to the type filter, with its channel hash and inner fields decoded. (#1791, 770749a8; #1792, d5ceb273)
  • CONTROL DISCOVER_REQ and DISCOVER_RESP subtypes decoded. (#1802, b3189c61)

Retention

  • observerPurgeDays: hard-delete observers that are already inactive and unseen for N days, but only when no observation, metric or dropped-packet row still references them. Disabled by default. (#1886, d821d9a3)

Fixes

Correctness

  • A retained MQTT status message is not observer liveness. Without this, last_seen was measuring broker restarts. (#1885, 9bd5f5a3)
  • Do not attribute a transported scope from a 1-byte hop prefix. (#1902, 4c45dec7)
  • Key the zero-hop advert skip on the path byte rather than the route type. (#1913, 97b60903)
  • Keep resolved full-pubkey hops across a path-hop index rebuild. (#1904, f081f91b)
  • Decode the ANON_REQ source pubkey instead of treating it like REQUEST. (#1864, 0d6f59ab)
  • Restrict per-node clock skew to self-originated adverts. (#1816, #1818, 0352c9a2)
  • Count only live observers in the store's /api/stats query. (#1888, b3a306b8)
  • Preserve the firmware-default Public channel (0x11) in analytics. (#1729, 6a32ec2b)
  • detectSchema fails loud instead of caching the wrong schema mode. (#1901, 14417349)
  • Relay Airtime Share uses preamble-aware LoRa time-on-air. Partial fix. (#1768, 57956712)
  • Clear a stale "varies" hash size once a node settles. (#1726, f0763aec)

MQTT and ingest stability

  • Decouple the watchdog emit from blocking I/O. This is the root cause behind #1749. (#1749, 52d08214)
  • Escalate a persistent paho disconnect, recover from an emit panic, expose the watchdog tick. (#1749, 242c7c60)
  • Stop the watchdog force-reconnect from racing paho's own retry loop. (#1897, 647841c9)

Security

  • WebSocket CheckOrigin allowlist, blocking cross-origin scrapers. (#1793, ec0ebeda)
  • Drop the hardcoded og:url so shared links stay on your instance. (#1890, c5a71b34)

Interface

  • Relay-aware staleness for infrastructure nodes: dim rather than delete. (#1598, 4fc42d30)
  • Restore the Live map viewport from the lat, lon and zoom hash parameters. (#1709, 4d2033da)
  • Surface the Coverage route in the mobile navigation when it is enabled. (#1783, 55e203a9)
  • Cross-navigation links between observer and node detail pages. (#1825, ba68069c)
  • Dark-theme role swatches via per-theme CSS tokens. (#1715, 735d9eb5)
  • Do not re-render node dots while scrubbing the Live timeline. (#1754, 1adb0116)
  • Firmware and Client columns on the observers table. (#1789, 9757178a)
  • Accessibility: WCAG AA contrast repairs and expanded axe route coverage across the analytics tabs. (#1719, a344ae0a; #1705, 293efdb6; #1706, cbe6e94b)

Performance

  • Prepared statements for frequently-called server DB queries. (#1878, 2f711eb8)
  • Index, cache and deflake the /api/channels queries. (#1887, 89544b1d)
  • Use the cached ParsedDecoded() instead of repeated json.Unmarshal. (#1871, f49e3fcc)
  • Avoid a per-observation SQL fetch in the handleObserverAnalytics hot loop. (#1827, 17200602)
  • Reuse the ctx buffer in resolvePathForObs and cache ReadMemStats per store. (#1873, ac6fbaf9)
  • Replace idx_tx_last_seen with a partial index on last_seen = 0. (#1740, e465e1c6)
  • Cap the Live animation canvas at DPR 1.5 and redraw at about 60 fps. (#1737, c03f2ebb)

CI

  • Unblock the master pipeline: gate the staging deploy and detach the badges job. A hung self-hosted job had been holding every master run open. (#1938, 859173f1)
  • Pin the packets time window in the slide-over E2E. The fixture aged out of the 15-minute default window before the flake gate ran, which had been failing unrelated PRs for weeks. (#1923, 589fa987)
  • Report the correct version on fast-path retagged images. (#1807, 9ef4179e)

Operator notes

The first one needs action; the rest are defaults you may want to reconsider.

  1. Add a CARTO key. Set map.tiles.providers.carto.key in your config. Without it every tile is served watermarked with HTTP 200, so no error and no failing healthcheck will tell you. The query parameter is key; api_key is silently ignored and still returns the watermarked tile. The key is sent to the browser, so restrict it in the CARTO dashboard if your deployment allows that.

  2. pathTrust.minHashBytesForMapping defaults to 1, which is the behaviour you have today, so an upgrade changes nothing on its own. Raising it to 2 makes hop attribution stricter and stops counting 1-byte prefix hops as mapping evidence, including legacy persisted edges with prefixBytes == 0. That is not a small change: on one live instance 56% of path-hop observations are 1-byte and 41% of repeaters use a 1-byte hash. There is no UI to undo it, which is why the default was left where it was.

  3. New opt-in tables, all default OFF, each with its own retention window: clientRxCoverage, clientRxObservations and clientRfSamples, bounded by retention.clientRxDays, retention.clientRxObsDays and retention.clientRfDays. Enabling a stream without setting its retention window means unbounded growth.

  4. observerPurgeDays is new and disabled by default. Set it above both observerDays and packetDays: below those, the reference guards keep every row anyway.

  5. WebSocket origin allowlist. If you embed CoreScope from another origin, allowlist it or the socket is refused.

CI fixes carried in this release

These landed after the v3.10.0 tag and are why this release is numbered v3.10.1. None of them touch product code.

  • Skip the pipeline for documentation-only changes, and match root-level markdown in that filter. A 16-minute Playwright run to prove a text file does not break a browser was pushing the runs that matter down a queue that ran hours deep. (#1949; #1950)
  • Publish container images on a tag ref, not only on a push event. release-fast-path.yml falls back to dispatching the full pipeline when it cannot re-tag :edge, and that fallback had never been able to publish anything: every GHCR step was gated on github.event_name == 'push', and a dispatch is not a push. It built locally, reported success, and pushed nothing. (#1951)

Contributors

115 pull requests merged since v3.9.2, from 13 people.

PRs
Kpa-clawbot 46
efiten 26
ArcanConsulting 15 includes #1771, merged as #1928
SaarMesh-Bot 7 includes #1863, merged as #1930
Joel-Claw 4 includes #1878, merged as #1934
Jonher937 4 includes #1887, merged as #1936
Saarlandpower 4
TeTeHacko 3
nullrouten0 2 #1916 and #1917, merged as #1926 and #1935
Bjorkan 1
MarekWo 1
djkazic 1
h4badger 1

Six of these were rebased onto master by someone other than their author, because the original branch had gone stale while the queue was stalled. The rebase preserved authorship on the branch, but this repository squash-merges, and a squash takes the pull request author as the commit author. So git log credits the rebaser for all six. The table above corrects that, and the third column names which PR the work actually came from.

That matters most for nullrouten0, who has zero merged pull requests under their own name in this release, yet wrote the CARTO basemap fix that this release leads with. The same applies in smaller measure to ArcanConsulting, SaarMesh-Bot, Joel-Claw and Jonher937.

Counted by pull request, not by commit, and restricted to what merged after v3.9.2 was tagged on 2026-06-13.