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>
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.keyis 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_seenis written again. The server-side touch had been a no-op since themode=rorefactor, so a repeater'slast_seenhad 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 betweenlast_relayedandlast_seendrops 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.minHashBytesForMappingnow 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 inclient_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_7don the node detail endpoint. (#1831,d2ef624c)unscoped_relay_count_24hper 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.destHashandpayload.srcHashin filter autocomplete. (#1774,b3b8bec5)- A
routed_throughfilter, 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_REQandDISCOVER_RESPsubtypes 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_seenwas 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_REQsource pubkey instead of treating it likeREQUEST. (#1864,0d6f59ab) - Restrict per-node clock skew to self-originated adverts. (#1816, #1818,
0352c9a2) - Count only live observers in the store's
/api/statsquery. (#1888,b3a306b8) - Preserve the firmware-default Public channel (0x11) in analytics. (#1729,
6a32ec2b) detectSchemafails 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
CheckOriginallowlist, blocking cross-origin scrapers. (#1793,ec0ebeda) - Drop the hardcoded
og:urlso 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,lonandzoomhash 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/channelsqueries. (#1887,89544b1d) - Use the cached
ParsedDecoded()instead of repeatedjson.Unmarshal. (#1871,f49e3fcc) - Avoid a per-observation SQL fetch in the
handleObserverAnalyticshot loop. (#1827,17200602) - Reuse the ctx buffer in
resolvePathForObsand cacheReadMemStatsper store. (#1873,ac6fbaf9) - Replace
idx_tx_last_seenwith a partial index onlast_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.
-
Add a CARTO key. Set
map.tiles.providers.carto.keyin 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 iskey;api_keyis 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. -
pathTrust.minHashBytesForMappingdefaults 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 withprefixBytes == 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. -
New opt-in tables, all default OFF, each with its own retention window:
clientRxCoverage,clientRxObservationsandclientRfSamples, bounded byretention.clientRxDays,retention.clientRxObsDaysandretention.clientRfDays. Enabling a stream without setting its retention window means unbounded growth. -
observerPurgeDaysis new and disabled by default. Set it above bothobserverDaysandpacketDays: below those, the reference guards keep every row anyway. -
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
pushevent.release-fast-path.ymlfalls 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 ongithub.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.