Commit Graph
3145 Commits
Author SHA1 Message Date
efiten eda950fa65 Add CodeQL analysis workflow configuration 2026-11-09 23:32:01 +01:00
efitenandClaude Opus 5.5 1577b57d67 docs(accounts): approved channels are backfilled since #2133 (#2385)
`docs/user-guide/accounts.md` said that messages received before a
channel proposal is approved stay encrypted. Since #2133 that is no
longer the case: when the key set grows, the ingestor re-decodes the
stored rows it could not decrypt (`cmd/ingestor/key_backfill.go`), and
the server tails `tx_rewrite_feed`, so no restart is needed.

Checked on an instance running #2133: approving four hashtag channels
triggered one pass (`[key-backfill] channel: 4 of 410 key(s) not yet
applied to history`, then `pass complete, 801 row(s) rewritten`, 13 s),
and the 801 rows are exactly the stored messages of those four channels,
the oldest from three months before the approval.

Docs only, one sentence. Not done: I did not check the other docs pages
for the same claim beyond a grep for "stay encrypted" and "received
before" in `docs/` and `README.md`, which found only this one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 22:31:07 +01:00
Paul PicazoandClaude Opus 5.5 d66a219bcb feat(cors): let a UI on another origin connect, static host previews included (#2362)
## What this does

Server changes so the UI can be served from another origin, such as a
Cloudflare Pages project and its previews, against an existing backend.
It ships before the frontend, which is parked on
[`backup/static-frontend-1a`](https://github.com/ppicazo/CoreScope/tree/backup/static-frontend-1a)
and will come as its own PR.

- **Exposed headers:** allowed origins get
`Access-Control-Expose-Headers: Retry-After, X-CoreScope-Load-Status`.
The UI reads both, and cross-origin they are `null` without this.
- **Wildcard origins:** an entry may be `scheme://*.host[:port]`, such
as `https://*.myproject.pages.dev`. It matches exactly one leftmost
label (each deploy and branch preview) for both CORS and `/ws`, but not
`myproject.pages.dev` itself. The part after `*.` needs at least three
labels, so `*.pages.dev` is refused. Refused or malformed wildcards are
dropped at startup with a log line. That floor is a sanity check, not a
Public Suffix List lookup: `https://*.s3.amazonaws.com` passes and would
admit every S3 website endpoint, so a wildcard belongs only over a host
the operator controls.
- **Case:** exact entries now match case-insensitively for CORS (was
`==`), as they already did for `/ws`.
- **Test only:** pins that the three anonymous POSTs (`/api/decode`,
`/api/packets/observations`, `/api/paths/inspect`) accept a `text/plain`
JSON body, which keeps them free of a preflight.

Credentials stay off, so nothing a listed origin sends rides on cookies.
Beyond GET, a listed origin, and so every deployment under a wildcard
entry, can:

- call those three anonymous POSTs as simple requests. That predates
this PR; they compute an answer and write nothing.
`Access-Control-Allow-Methods: GET, HEAD, OPTIONS` only governs
preflighted requests.
- with `userManagement.enabled`, use the existing bearer tier for
companion linking (`/api/auth/device-token`, `/api/auth/me`,
`/api/auth/logout`, `/api/account/settings`, `/api/account/companions*`)
with POST, PUT and DELETE. Those routes need a device token in
`Authorization`, not a cookie, but a preview deployment gets them too.
On such a server, list a wildcard only if previews may use them.

## Configuring

```yaml
- CORS_ALLOWED_ORIGINS=https://existing-embed.example,https://myproject.pages.dev,https://*.myproject.pages.dev
```

The env var replaces `corsAllowedOrigins`, so keep the origins already
listed there.

## Testing

- `go test ./...`, `go vet` and `gofmt` are clean in `cmd/server`. The
new tests were written first and failed first.
- In Chromium, the static build from the backup branch, served on
`pr-1.proj.preview.localhost`, passed its split E2E (9/9, WebSocket
included) against this server.
- From the same setup, the project origin connected, a two-level
subdomain was blocked, and `X-CoreScope-Load-Status` read `ready` where
master gives `null`.

## Performance

These run per request in the CORS middleware and the `/ws` handshake,
and they allocate nothing. Same-origin requests skip them.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:46:17 +01:00
n30nexandn30nex 899a3ad0b1 fix(map): normalize saved viewport longitudes (#2378)
Partial fix for #2375. Restoring a saved longitude such as -352.7
previously put markers one world-width off-screen while tiles looked
correct. Saved positions now use Leaflet’s native wrapping and the
existing viewport validator; movement saves canonical coordinates.
Invalid saved values fall back to configured defaults. Valid URL
viewports and node highlighting keep precedence.

This repairs saved state, not live dragging across world copies. No new
settings, dependencies, requests, marker loops or render triggers; added
work is constant per initialization/existing moveend.

The reported-node URL diagnosis is separate: no-slash returns JSON when
enabled and intentionally 404 when disabled; trailing-slash returns
HTML. See #2372. The endpoint/gate remain unchanged; #2375 stays open
for maintainer reconciliation.

RED `cb453c0d` produced [14 passes and 10 intended CI
assertions](https://github.com/Kpa-clawbot/CoreScope/actions/runs/42317069830/job/128232241526).
Production fix `e6b70dfd` passed CI, 224 frontend suites, 24 browser
cases, six estimated-marker and three mobile checks. Marker placement
changes from x=262754/offscreen to x=610/visible in the same fixture.

Review follow-up `99a4746e` changes only test data: configured and saved
centers now differ, while invalid storage independently expects the
default. Parent rerun passes 24/24; a browser-only mutation discarding
saved coordinates now fails seven cases. All previous assertions and
production files are retained. The single review finding is resolved.

Browser verified: disposable Go fixture at `http://127.0.0.1:14956`,
real Leaflet rendering.
E2E assertion added: `tests/e2e/test-map-nodes-pagination-e2e.js:129`.

Final follow-up CI is blocked by the unchanged observer disposal
assertion (#2342; fix #2343). A maintainer must rerun the workflow; this
remains draft.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-11-08 17:40:40 +01:00
n30nexandn30nex 8420a7ac0f feat(map): identify historical foreign-advert flags (#2371)
Partial implementation of #730 M2; the issue remains open.

The map marks nodes whose stored `foreign` Boolean is true in dot and
hash-label modes. Titles, accessible names and popups explain that an
advert was flagged outside the configured area at ingestion. This
historical flag does not establish current location, bridging, an
observer or a recent count. Analytics/history remain separate.

The warning uses existing theme tokens and stays clear of nearby markers
and phone controls. Footprint preparation adds O(n) work and numeric
metadata to the existing marker list (around 2K nodes), with one
typography read per applicable render and no per-marker DOM measurement
or requests. Existing bounded-offset placement retains its worst-case
quadratic search; this is not a speedup claim. No new settings or render
triggers.

RED: `4ad11148` produced [44 missing-warning
assertions](https://github.com/Kpa-clawbot/CoreScope/actions/runs/42012004213/job/127193151694).
Follow-up `c4a157de` produced [11 warning-occlusion
assertions](https://github.com/Kpa-clawbot/CoreScope/actions/runs/42023035345/job/127230364384).

GREEN `ad465e32`: parent browser check 66/66, 224 frontend suites, 38
existing map/route checks and CSS audit. Incorrect-copy controls also
prove the tests reject a current-location inference.

Browser verified: local Go fixture, desktop/phone, both themes. E2E
assertion added: `tests/e2e/test-map-foreign-advert-e2e.js` covers
keyboard access, missing/false flags, no-GPS, refresh, clustering,
overlays, zoom and resize.

## Preflight overrides

- check-red-commit: initial RED includes the new browser regression plus
necessary CI/inventory registration, with no production changes; CI
reached the intended assertions.

[Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/42026358900)
passes. Three independent reviews were consolidated; both findings are
fixed and parent-verified. Kent Beck Gate: PASS.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-11-08 17:13:33 +01:00
CoderNemesis 65990873ad feat(map): deep-link the route path picker by observer or All view (#2376)
## Summary

The map route deep link (`#/map?packet=<hash>[&obs=<id>]`) always opened
in the canonical-path view. Two optional query params now choose the
path picker's starting view, so a shared link shows exactly what the
sender meant.

| Param | Effect |
|---|---|
| `paths=all` | Opens the picker's **All** view (every unique path), the
same as clicking **All** |
| `observer=<pubkey>` | Full public key or a hex prefix of 8+
characters, case-insensitive. Picks that observer's **earliest**
observation (by timestamp, then id), makes it the canonical route and
isolates its path row, the same as clicking the row. "Back to packet"
then links to that observation. |

Examples:

```
#/map?packet=974ad4ecefbe3761&paths=all
#/map?packet=974ad4ecefbe3761&observer=a1b2c3d4
```

When params are combined, `obs=` wins over `observer=`, and `observer=`
wins over `paths=all`. An unknown or malformed observer falls back to
the current default view and logs a console warning.

## Changes

- `public/route-view-utils.js`: two new pure helpers.
- `resolveDeepLinkObservation(observations, params)` picks the
observation and the initial view.
- `pathGroupKey(rawHops, filterHops)` is the path picker's grouping key.
It was written out twice in `route-view.js` and now lives in one place,
which keeps the deep link and the picker rows in sync.
- `public/map.js`: `loadRouteFromDeepLink` uses the helper and passes
`initialPathView` through `drawPacketRouteMulti` to the renderer.
- `public/route-view.js`: the picker count and rows use `pathGroupKey`.
`sidebar._applyPathView` applies the view through the existing All and
row click handlers, in the frame after the route's initial fit.

## Performance

This doesn't touch a hot path. The work is one pass over a single
packet's observations (tens at most) plus a scan of the picker rows, and
it runs once per deep-link load. It adds no API calls.

## Config / customizer

No new configurable values.

## Tests

- New unit suite `tests/unit/test-map-deeplink-path-params.js` (29
checks). It covers prefix vs full key, case, short/non-hex keys,
earliest-observation choice with tie-break, the precedence rules,
`pathGroupKey`, and the source wiring.
- `tests/unit/test-issue-1633-hide-1byte-hops.js` replayed a copy of the
key logic. It now tests the real `pathGroupKey`.
- New Playwright suite `tests/e2e/test-map-deeplink-path-params-e2e.js`
(5 checks) on the seeded #1486 packet. It's registered in
`scripts/non-unit-tests.json` and CI shard 3. With the `route-view.js`
change removed, 3 of its 5 checks fail.
- Local runs against the CI-prepared fixture: `test-all.sh` passes.
Route-map a11y E2E 20/20, map mobile E2E 3/3, `test-e2e-playwright.js`
149/152 (3 skipped by the fixture, 0 failed).

## Browser validation

Checked in a browser against a local server, using a scratch copy of the
fixture that gives the seeded packet multi-hop paths through real GPS
nodes. One observer had two observations, and the API listed the later
one first.
- `observer=<prefix>` picked that observer's earliest observation and
drew only its path, with numbered markers and the map fitted to it.
- `paths=all` drew the same picture as clicking **All** by hand.
2026-11-08 17:13:22 +01:00
Marek WojtaszekandClaude Opus 5.5 3100d7a4c9 fix(ui): route remaining payload label surfaces through payload-labels.js (#2373) (#2374)
Fixes #2373

## Summary

Finishes the #1799 / #1804 migration: every frontend surface that names
a payload type now reads it from `public/payload-labels.js`. Node
Analytics, Observer detail and Observer comparison showed GRP_DATA,
MULTIPART and RAW_CUSTOM as `Type 6` / `Type 10` / `Type 15`. The map
route sidebar had RAW_CUSTOM on id 12; the firmware uses `0x0F`.

## Changes

- **`public/payload-labels.js`**: new
`PayloadLabels.api.displayName(id)` returns the canonical short label,
or `Type N` for ids the firmware does not define (e.g. a corrupted
header byte stays visible as `Type 13`). It lives on `.api` only; the
legacy root aliases are not extended.
- **`public/node-analytics.js`, `public/observer-detail.js`,
`public/compare.js`**: drop the local `PAYLOAD_LABELS` maps and use
`displayName()`.
- **`public/nodes.js`**: drop the local `PAYLOAD_TYPES` map; Recent
Adverts uses `shortById()` and keeps its `Packet` fallback.
- **`public/app.js`**: drop `PAYLOAD_TYPES`; `payloadTypeName()` uses
`shortById()` and keeps its `UNKNOWN` fallback.
- **`public/map.js`**: drop `PAYLOAD_TYPE_MAP`; `inferredType` uses
`enumNameById()`, which fixes RAW_CUSTOM (15 → `RAW_CUSTOM` instead of
`OTHER`).
- **`public/packets.js`**: `typeName()` is now `displayName` (same
output, one implementation).

Consumers resolve labels at render time rather than at file load, so the
many unit sandboxes that load these files without `payload-labels.js`
keep working. `index.html` already loads `payload-labels.js` before all
of them.

## Tests

- **New `tests/unit/test-payload-labels-surfaces.js`** (listed in
`test-all.sh`):
- drift guard: fails if any `public/*.js` other than `payload-labels.js`
pairs a numeric id with a short label or firmware enum name (it caught
the `map.js` copy, which was not on the original list);
- `displayName()` for every enum, string ids, and the `Type N` fallback;
- loads the real `node-analytics.js` in a vm sandbox and asserts the
doughnut labels for ids 6, 10, 15 and 13.
  - Before the fix: 11 failures. After: 82 passed.
- **`tests/e2e/test-issue-1799-label-vocab-e2e.js`**: two new steps
intercept `/api/nodes/:pk/analytics` and `/api/observers/:id/analytics`
(the fixture has no GRP_DATA in the store), inject ids 6, 10, 15 and 13,
and read the labels from `Chart.getChart()`. Against upstream `master`:
11 passed, 2 failed (`["Type 6","Type 10","Type 15","Type 13"]`). With
this branch: 13 passed.
- **Sandbox updates**: `test-compare-overlap.js` and
`test-observer-detail-ownership.js` now load `payload-labels.js` before
the page script, as `index.html` does.
`test-issue-1418-raw-hex-extraction.js` asserted the hand-rolled
`PAYLOAD_TYPE_MAP` literal; it now asserts that `inferredType` uses the
canonical map and that no local map exists.
- Unit suites, each `test-all.sh` entry run on its own: 223 pass on this
branch vs 222 on `master` (+1 new suite). `test-manage-workflow.js` and
`test-preflight-xss-gate.js` fail identically on `master` in my Windows
environment (CRLF in the shell fixture, no `python`); they are unrelated
to this change.
- E2E, run against a local server on the CI fixture (freshened, seeded
and migrated as in `deploy.yml`): `test-issue-1799-label-vocab-e2e.js`,
`test-charts-fluid-1058-e2e.js`, `test-issue-1147-section-order-e2e.js`,
`test-issue-1640-compare-discovery-e2e.js`,
`test-issue-1648-m2/m3-icons-e2e.js` and
`test-observer-detail-ownership-e2e.js` all pass.
`test-e2e-playwright.js` fails one step (`Version info lives on Perf
dashboard`, `#navStats` wait timeout) with the same result on `master`
locally.

## Performance

No hot-path change. Every lookup is still one object-key read per
packet: the packets-table sort accessor calls `displayName(t)` instead
of `TYPE_NAMES[t] ?? ...`. No new loops, requests or allocations.

## Browser validation

Checked against the local fixture server, with the analytics responses
patched in the page to contain GRP_DATA:

- Node Analytics → Packet Types legend: `Group Data, Advert, Channel
Msg, Direct Msg, Multipart, Raw Custom, Type 13`
- Observer detail → doughnut and the first Recent Packets row read
`Group Data`
- Observer comparison → packet table rows read `Group Data`
- Map route deep link (`#/map?packet=…`) renders the route panel; no new
console errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:13:10 +01:00
n30nexandn30nex 6618820836 fix(nodes): show timestamp context for Last Contact (#2377)
Partial fix for #1154 U10. Peer Interactions Last Contact now shows
timestamp context through the existing shared formatter: relative text
with an absolute tooltip, or the reverse when absolute display is
selected. Existing timezone, precision and custom-format preferences
apply; missing/invalid values remain a dash. Other U10 surfaces and
tracking items stay open.

The production change only formats the existing cell. Work stays O(P)
over the existing maximum 20 peers; no requests, settings, dependencies,
caches or render triggers are added. Tooltip and text are HTML-escaped.

RED `c47cce20` reached [11 unit assertion
failures](https://github.com/Kpa-clawbot/CoreScope/actions/runs/42316165311/job/128229216777)
and [six browser missing-tooltip
assertions](https://github.com/Kpa-clawbot/CoreScope/actions/runs/42316165311/job/128229216764).
GREEN `2c29eccb` keeps those tests unchanged and passes 82 unit
assertions, 10 focused browser cases (parent rerun), and all 224
frontend suites. Existing browser smoke reports 149 passes and three
fixture skips. One separate navigation timeout passed on an unchanged
serial retry.

Browser verified: disposable Go fixture at `http://127.0.0.1:14953`,
desktop/phone; tests retain the real API response and supply
deterministic peer contacts. Its unrelated missing battery-metrics table
is a fixture limitation.
E2E assertion added:
`tests/e2e/test-issue-2218-node-analytics-stale-e2e.js:123`.

[Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/42317025356)
passes. Independent adversarial, UI and test-quality reviews found no
must-fixes; Kent Beck Gate: PASS.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-11-08 17:13:00 +01:00
n30nexandn30nex 0e2ed89829 fix(packets): explain observer-report coverage (#2370)
Red commit: `e86294b6` ([four intended CI assertion
failures](https://github.com/Kpa-clawbot/CoreScope/actions/runs/42009863423/job/127185896565)).

Partial fix for #667, Part B. The Packets page now explains that its
counts come from observer reports received by this instance—a partial
view, rather than network-wide totals or device firmware counters. The
note remains visible on phones and describes the table for assistive
technology.

This does not change TX/RX storage, counts, filtering or grouping, and
leaves the rest of #667 open. It invents no contributing-observer number
and makes no RX-only claim. No new settings, dependencies, requests or
processing work.

Validation on `08036329`: parent-run browser regression 10/10; required
frontend unit suites 932 assertions. The RED run completes six existing
cases and fails the four new missing-context assertions.

Browser verified: disposable local fixture at `http://127.0.0.1:14943`
(desktop, phone, populated/empty filters, grouping and refresh). E2E
assertion added: `tests/e2e/test-issue-1122-packets-filter-ux-e2e.js`.

Updated CI passes; independent adversarial, UI and test-quality reviews
found no must-fix issues.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-11-08 17:12:49 +01:00
199df07c21 docs(proto): correct route_type values to match the decoder (#2363) (#2367)
Fixes #2363.

The published contract described `route_type` as `0=DIRECT, 1=FLOOD,
2=reserved, 3=TRANSPORT`. Both decoders (`cmd/ingestor/decoder.go`,
`cmd/server/decoder.go` — `routeTypeNames`) and MeshCore's
`docs/packet_format.md` use:

```
0 = TRANSPORT_FLOOD, 1 = FLOOD, 2 = DIRECT, 3 = TRANSPORT_DIRECT
```

Updated:
- `proto/decoded.proto` — `DecodedHeader.route_type` and the
`route_type_name` value list
- `proto/packet.proto` — `Transmission.route_type` (same stale comment)
- `docs/api-spec.md` — Packet Object `route_type` line (same stale
comment)

Comment/doc-only; no generated code in-tree, no behaviour change.

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

Co-authored-by: SaarMesh-Bot <300107934+SaarMesh-Bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-08 17:12:37 +01:00
n30nexandn30nex c4dc46f349 test(e2e): wait for comparison focus before disposal assertions (#2343)
Red commit: `eedadc43` ([four intended CI assertion
failures](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40586787653/job/122480320591)).

Fixes #2342. The disposal test could capture comparison results before
the router's accessibility-focus frame added the heading tabindex, then
mistake that legitimate setup change for a late observer response.

Wait for the actual tabindex in attached state before capturing the
baseline, including when the mobile heading is hidden. A controlled
late-focus variant proves the regression. Full HTML equality, chart
cleanup, route and control assertions remain intact; no production
behavior or settings change.

Current head `cb537ebe` includes current master while preserving the
original RED/GREEN commits and newer progressive-render coverage. Local
validation: ownership browser suite 60/60 (independently rerun by
parent), navigation 60/60, branding 17/17 and all 224 standalone
frontend suites.

Browser verified: real local Go fixture server at
`http://127.0.0.1:14944`. E2E assertion added:
`tests/e2e/test-observer-detail-ownership-e2e.js`.

Updated CI passes; independent adversarial, UI and test-quality reviews
found no must-fix issues.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-11-08 17:12:03 +01:00
JadeandClaude d1bf4ca042 fix(docker): build natively on arm64 hosts (#2359)
Building the image natively on an arm64 machine (a Raspberry Pi 4 or 5
on 64-bit OS, Apple silicon) fails at the first `RUN` of the build
stage:

```
=> ERROR [builder  2/40] RUN apk add --no-cache curl xz && ...
0.841 exec /bin/sh: exec format error
```

**Cause:** the Dockerfile declares `ARG BUILDPLATFORM=linux/amd64`. A
default on that ARG overrides the value BuildKit sets for the machine
doing the build, so an arm64 host pulls the amd64 `golang` image and
can't run it. CI didn't catch it because its arm64 image is cross-built
on an amd64 runner, where `linux/amd64` was correct anyway.

**Fix:** declare `ARG BUILDPLATFORM` without a default. BuildKit, the
default builder since Docker 23 and what buildx uses, fills it in on
every host. amd64 builds and the CI cross-build are unchanged.

**Workaround on current master:** `docker build --build-arg
BUILDPLATFORM=linux/arm64 .`

**Test:** `tests/unit/test-dockerfile-buildplatform.js` fails on the old
Dockerfile and passes on this one. It checks three things:
- `BUILDPLATFORM` has no default.
- The Go builder stays pinned to `$BUILDPLATFORM`.
- No stage hard-codes a platform.

It's wired into `test-all.sh`.

Found while building the image on a Raspberry Pi running Raspberry Pi OS
Trixie (arm64) with Docker 29.


---

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 12:13:29 +01:00
n30nexandn30nex 9341ec744b fix(observers): honour node visibility in neighbour reports (#2357)
Fixes #2356.

Observer neighbour reports now apply the existing node blacklist and
hidden-name-prefix rules before returning displayed identities. Names
are already batch-loaded, so the fix filters the request-owned slice
without another SQL query. It preserves reported totals, timestamps,
stored data, public/unknown neighbours and the existing reporter-only
observer-blacklist semantics. An all-hidden list remains `[]`.

The work is bounded by the existing 500-entry ingestion cap plus the
existing visibility checks; no new configuration, frontend change or
database write is introduced. The filter reuses the existing report
slice.

Validation:

- [RED
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40971452075/job/123789013428)
at `7341b334` compiled and reached the four expected visibility
assertions.
- GREEN `b38e95e6`: focused native checks passed in 6.607 s; parent
rerun passed in 1.865 s.
- Full native server passed in 189.743 s; parent ingestor suite passed
in 399.859 s. Existing neighbour frontend checks passed 7/7.
- Tests use the actual registered router, direct-node 404 controls,
casing/shared-name/unknown-node cases, metadata preservation and an
unchanged stored report.

Local standalone browser replay is unavailable because the
fixture-server launch was blocked. Fresh GREEN CI passed, including
server race checks and browser shards. Independent adversarial,
security-boundary and TDD reviews found no must-fix items. No
live-instance incident or deployment is claimed.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-10-31 12:01:19 +01:00
n30nexandn30nex d083f9ade9 test: cover partial observer chart construction failures (#2354)
Partial follow-up for #1778; addresses the chart-cleanup test gap
identified in the review of #2339.

The observer detail unit harness now throws during the second or fourth
chart constructor. Both cases require existing chart instances to be
destroyed, metadata/calibration to remain visible, the error to be
rendered as text, and a later time-window retry to recover without extra
requests.

Production cleanup already behaves correctly; this PR adds coverage
only. The parent-run ownership suite passes 30/30. Removing the actual
cleanup produces exactly the two intended assertion failures. The
remaining unit sources and runner match the current-master baseline
whose 222 frontend suites passed; only this updated unit file differs
within that test set.

TDD scope: no production change or new behavior. The negative control
removes existing cleanup to prove the new assertions detect its loss. No
configuration, dependencies or runtime performance changes. Exact-head
CI passed, including the browser shards. Independent adversarial,
correctness and TDD reviews found no must-fix items.

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-10-31 12:00:06 +01:00
n30nexandn30nex b3a9f2a9e9 docs: make scheduled backups portable and bounded (#2353)
Fixes #2352. Follow-up to #2345.

The older deployment guide recommended copying a live telemetry
database. It now points to the consistent-snapshot procedure. The
scheduled-download example uses a trailing-X temporary filename, bounds
connection and total download time, and explains the weak-key denylist.

Validation: the parent reran the extracted command against eight
disposable HTTP cases: valid SQLite, unauthorized, server error, empty
content, HTML, corrupt SQLite, truncated transfer and timeout. Failed
downloads removed partial files and preserved existing completed
backups; the timeout exited 28. Shell syntax and diff checks passed. GNU
tools were exercised; BSD/macOS was not run natively.

TDD exemption: pure documentation, no application or repository test
files changed. No runtime/browser behaviour, dependencies or
configuration changed. Independent reviews are complete; the one
timeout-wording finding was addressed in 33d629b5 without changing the
validated command. Exact-head CI passed, including the browser shards.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-10-31 11:59:39 +01:00
n30nexandn30nex f8c3b32633 docs(api): describe readiness responses and ingestor liveness (#2358)
Partial follow-up for #1670.

Documents public `GET /api/healthz`: 503/loading, current in-memory
counts, the fixed legacy backfill compatibility object, and optional
ingestor liveness timestamps. Receipt and handler-entry clocks have
different reset semantics; handler entry does not prove a completed
database write. The legacy object does not track real migration
progress.

The 200 response has a typed schema, including its dynamic source-name
map. Only `/api/healthz` leaves the known-gap list (16 → 15). Handlers,
configuration and frontend behavior are unchanged; formal non-200
response-schema work remains separate.

Validation:

- The new documentation assertion failed before metadata was added;
actual handler-response checks passed.
- Full native server/ingestor suites passed in 257.438 s / 452.613 s.
- After review corrections, focused checks passed in 2.757 s and the
parent rerun in 2.097 s, including exact compatibility values and
retained-receipt/zero-handler timestamps.
- Independent adversarial, correctness and TDD reviews completed; the
parent verified both semantic corrections against actual call sites.
- [Exact-head CI
passed](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40979351261),
including browser shards.

TDD exemption: documentation/API metadata only. Local standalone browser
smoke was blocked; no local-browser or deployment claim is made.
Production untyped-map count is unchanged.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-10-31 11:59:20 +01:00
efitenandClaude Opus 5.5 faf6f792ae docs(release): v3.16.0 release notes and changelog entry (#2355)
Release notes for v3.16.0 (`docs/release-notes/v3.16.0.md`) and the
matching `CHANGELOG.md` entry. The tag pipeline uses this file as the
GitHub release body.

Range `v3.15.0..af2e96d9`, 47 commits: 23 fix, 9 feat, 6 docs, 2 test, 2
perf, 2 ci, 1 ui, 1 deps, 1 chore.

"Read this before upgrading" covers three changes:
- relay hops refresh Last Heard only with a unique prefix and only on
flood packets (#2334, #2308, #2298, #2336);
- `users.db` schema v6 to v8 with user accounts on (#2306, #2348);
- Go 1.26 for source builds and Alpine 3.24 in the image (#2297).

Not verified: the mosquitto, Caddy and supervisor versions are taken
from #2297's local build, not from a pull of the published image.
Performance numbers are copied from the PRs, not re-measured.

Docs only, no code change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v3.16.0
2026-10-29 23:11:30 +01:00
af2e96d92a fix(reach): queue a cold request up to 3 s for a build slot before answering 429 (#2249)
Follow-up to #2127, from @dborup's review there. The limiter, most of
its tests and the docs text are dborup's (dborup/CoreScope@195ec4c3),
adapted to master; the commit carries `Co-authored-by: dborup`.

## What changes

`GET /api/nodes/{pubkey}/reach` still runs at most 2 uncached reports at
once. What changes is the next uncached request:

- **Before:** 429 at once, `Retry-After: 5`.
- **After:** it waits up to 3 s for a slot. It gets 429 with
`Retry-After: 2` and a JSON body only if no slot frees in that time.

Unchanged: cached answers never wait, and concurrent requests for the
same node and `days` share one computation and one slot.

One addition beyond dborup's version: the shared computation runs on the
context of the request that started it. If that request disconnects,
while queued or mid-scan, the other requests sharing it that are still
connected now get 429 instead of 500. The 3 s wait made that case more
likely.

## Why

dborup's three points on #2127:

1. An instant 429 fails two tabs, or a quick switch between 7/14/30
days, when the database is not under real pressure.
2. The page cannot recover from it, because `api()` retries only 503.
The panel retry is a separate PR so that this one stays server-only.
3. No test covered the promised property: a cached request still answers
while every slot is held.

Waiting parks a goroutine, not a database connection, so the pool is
protected exactly as before.

## What I checked

- **Tests.** `make test` (all 16 modules), `make vet`, `gofmt`, `sh
test-all.sh` and eslint (0 errors) all pass. The reach tests also ran 10
times under `-race`.
- **New tests.** 14 tests in `node_reach_cold_limit_test.go`: dborup's
10 plus 4 new. `TestNodeReachConcurrentBuildCap` now drives the new
limiter through the production router. Point 3 is
`TestNodeReach_WarmHitSkipsColdLimit`. The 4 new tests are:
  - a queued request is served when a slot frees;
  - a cache filled just before the flight skips the limiter;
- coalesced waiters get 429 when the starter disconnects, queued or
mid-scan (two tests).
- **Mutations.** I wrote 21 from the code before running the tests, and
the tests caught all 21: instant reject, cap +1, release that leaks,
slot taken before the in-flight recheck, busy → 500, Retry-After header
or value, status 503, dropped context mapping, and others.
- **Real binary.** Master `2d09335c` and this branch, run one after the
other on a copy of a 10.5M-observation, 11 GB database. Each burst fires
its reach requests at once, while `/api/nodes?limit=50` and
`/api/packets?limit=50` are polled every 200 ms.

| Burst (distinct uncached keys) | master | this branch |
|---|---|---|
| 3 keys, `days=7`, warm page cache (2 runs) | 2 × 200, 1 × 429 after
0.01 s | 3 × 200; the third answered at 4.1–4.3 s |
| 6 keys, three of them `days=14` (2 runs) | 2 × 200, 4 × 429 after ≤
0.01 s | 2 × 200, 4 × 429 after 3.0 s |
| 24 keys, cold page cache | 2 × 200, 22 × 429 | 2 × 200, 22 × 429 after
3.00 s |
| `/api/nodes` during every burst | median 1 ms, max 53 ms | median 1
ms, max 66 ms |

## Not checked

- **Slow scans.** On a database this size, uncached scans took from
about 9 s to over 60 s, so the 3 s wait only helps when page cache is
warm or the database is smaller. Otherwise the result is the same 429, 3
s later. A 30-day uncached scan hit the 60 s `WriteTimeout` on both
builds; that is the scan #2214's opt-in hop index addresses.
- **Pool contention under a live store.** On this copy the in-memory
store loaded 0 packets, so `/api/nodes` latency does not show contention
under a live load.
- **Platforms.** One machine (linux/amd64). No browser check: the server
change has no UI, and the panel retry comes with its own PR.

**Base.** Rebased on master `7c00a429`, which includes #2214; every test
and mutation above was re-run on the rebased commit. The real-binary
runs used `2d09335c`, just before #2214, on a database without the hop
index. That is the default after #2214 too (`hopIndexDays` unset means
off). With the index on, scans are shorter, so more bursts fit in the 3
s wait; I did not measure that.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


---

## Update after review (commits `c65f4d3b`, `1d2cd99d`, `04e49854`)

efiten asked how many requests may wait at once. With only the 3 s
timeout, the number of waiting distinct keys grew with the request rate.
n30nex wrote a cap (nullrouten0/Baymc-CoreScope#1, tracked in #2264).
Its two commits are now on this branch unchanged, as a fast-forward.

- At most 8 distinct cold requests wait for a slot. Any further one gets
429 with `Retry-After: 2` at once, before a timer is set.
- A waiter gives its place back on every exit: it gets a slot,
disconnects or times out.
- Cached answers and same-key sharing are unchanged. Requests for a key
that is already queued or building wait on that one place and are not
counted. `04e49854` corrects the code comment that said the ceiling was
independent of request rate.

**What I checked on `04e49854`:**
- The two new tests fail on `28da016f` and pass now.
- 12 mutations of the cap, all caught: cap 7 and cap 9; the place never
returned, returned before waiting, returned only on promotion, leaked on
cancel, leaked on timeout; a full queue that blocks, returns the wrong
error or skips the counter; a timer armed before the check; a queue
sized by the slot count.
- `make test`, `make vet`, `make fmt-check`, `sh test-all.sh` and eslint
8.57.1 (0 errors) pass, and the reach tests pass 5 times under `-race`.
On a local merge with current master `660cbbf5`, which includes #2277,
the reach tests pass 3 times under `-race` and the `cmd/server` package
passes.
- Real binary: both heads, alternately, on a copy of a 2.5 GB soak
database, with 24 distinct uncached `days=7` requests fired at once, two
runs each:

| | `28da016f` | `04e49854` |
|---|---|---|
| 200 | 2, 2 | 2, 3 |
| 429 at once | 0, 0 | 14, 14 |
| 429 after 3 s | 22, 22 | 8, 7 |

**Not checked:** a sustained flood, a burst of same-key requests against
the cap, `/api/nodes` latency during these bursts, and platforms other
than linux/amd64. The real-binary table further up was measured before
the cap.

---------

Co-authored-by: nullrouten0 <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: dborup <kontakt@meshview.dk>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: n30nex <blushellneon@outlook.com>
Co-authored-by: efiten <erwin.fiten@gmail.com>
2026-10-29 22:50:59 +01:00
efitenandClaude Opus 5.5 bb3c8cc001 fix(rx-coverage): My coverage and the observer filter on the noise layer (#2326)
"My coverage" and a selected observer only narrowed the signal layer on
the coverage page. On the noise layer the toggle stayed pressed, but
`/api/rf-noise` was called without `mine` or `rx` and returned every
companion's samples.

## Changes

- **Backend:** `/api/rf-noise` now accepts `rx` and `mine=1`, with the
same session rules as `/api/rx-coverage`: 401 without a session, 403 for
a device token, 404 with user management off. The `rx_pubkey` filter
moved into 1 helper, `appendRxFilter` (`cmd/server/rx_dashboard.go`),
which both queries use. Both parameters are in the OpenAPI entry.
- **Frontend:** `public/rx-coverage.js` fetches both layers through
`fetchLayer`. It builds the URL with the time window, observer and
toggle, and judges the reply with the existing `coverageReply`. The
request sequence (`signalSeq`, now `drawSeq`) covers both layers, so the
noise layer also drops a superseded reply. Before, a slow unfiltered
noise reply could overwrite a newer filtered one.

## Performance

The noise query gets 1 extra `WHERE` clause on `rx_pubkey`. There are no
new requests: the page still makes 1 request per redraw.

## Verification

- New Go tests in `cmd/server/rf_noise_mine_test.go`: `rx` narrows the
result (case-insensitive), `mine=1` returns 401 without a session,
returns empty with nothing linked, returns the linked companion's cells,
returns 403 for a device token, and returns 404 with user management
off. Both failed before the fix.
- `tests/unit/test-rx-coverage-mine.js` is updated for the new
`coverageUrl(path, ...)` signature and checks that both layers fetch
through `fetchLayer`.
- New step in `tests/e2e/test-rx-coverage-noise-e2e.js`: a
`?layer=noise&rx=...` link sends `rx`, and clicking "My coverage"
refetches with `mine=1`. It fails against the current master frontend
and passes with this change.
- Local runs (Windows): `go test` and `go vet` in `cmd/server`,
`test-all.sh`, and the noise, gaps and viewport E2E in Chrome. All
green. I did not run the full Playwright suite locally.

## Not in this PR

- The "nothing received" gap cells still skip `mine=1`, unchanged: the
track query cannot narrow to a set of companions yet.
- The time window (`days`) already worked on the noise layer, so it is
not touched.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:39:02 +01:00
TeTeHackoandClaude Opus 5.5 417c8dd00a feat(observers): latest /neighbors report on the observers list and page (#2270, #2271) (#2300)
Closes #2270, closes #2271.

The ingestor took only the confirmed region scopes out of an observer's
`/neighbors` report (#1865) and dropped the rest. Nothing could
therefore say when an observer last reported, or whom it heard.

## Ingestor

`observer_neighbor_reports` (created in `dbschema.Apply`) keeps the
latest report of each observer, one row per observer, replaced by every
live report.

The report is unauthenticated, so `neighbors[]` is stored sanitised:

- only 64-hex keys
- `snr` and `heard_secs_ago` finite and in range
- `status` limited to `[a-z_]`
- `scopes` clamped
- at most 500 rows

`total_neighbors` is believed only when it is not below the number of
rows that arrived.

A **retained replay** only fills a missing row, and it is dated by the
report's own `timestamp`. After an ingestor restart, a long-dead
observer therefore does not look as if it just reported.

## API

The table is optional for the server (no `AssertReady` change); without
it the fields are simply absent.

- `GET /api/observers`: `neighbors_report_at` and `neighbors_count`,
from one batch query in the existing cached builder.
- `GET /api/observers/{id}`: also `neighbor_report`, which is
`{received_at, reported_at, neighbor_count, queried_neighbors,
self_scopes, self_default_scope, neighbors: [{pubkey, name, role, snr,
heard_secs_ago, status, scopes}]}`. Names and roles come from the node
table in one query.

## Frontend

`public/observer-neighbors.js` is shared by both pages and escapes
everything.

- **#2270, observers list:** a sortable **Neighbors** column, e.g. `3h
ago · 12`, or `—` when the observer never reported. The same value
appears in the slide-over.
- **#2270, observer page:** **Neighbors Report** and **Neighbors** stat
cards.
- **#2271, observer page:** a **Neighbors** table listing each neighbor
by name (a link to the node) or key prefix, with SNR, heard, scope query
status and scopes. The heading says when the list is shorter than the
count, e.g. `(2 of 3 listed)`.

## Tests

- **ingestor:** sanitising, the cap, live replace, the retained rules
(fill only, dated by the report, undated ignored) and the MQTT topic end
to end. The existing #1865 scope tests are unchanged and still pass.
- **server:** the list summary, the detail with names, and both
endpoints without the table.
- **frontend:**
  - unit tests of the helpers
- an XSS case for every rendered field; I checked that it fails when
escaping is removed
- `test-observers-headings` now expects the new column after `Last
Packet`
- **E2E:** `test-observer-neighbors-2270-e2e.js` intercepts the observer
endpoints, since the fixture has no reports. It is listed in
`scripts/non-unit-tests.json` and on shard 2.

Gates that pass:

- `go test -race` in `cmd/server`, `cmd/ingestor` and
`internal/dbschema`
- `sh test-all.sh`
- `eslint@8`, with 0 errors
- the XSS diff gate
- these E2E suites: the new one,
`test-issue-1639-observers-sort-e2e.js`,
`test-observer-iata-1188-e2e.js` and `test-e2e-playwright.js`

Performance: the list adds one primary-key-sized query per cache fill,
and the detail adds two small queries per request.

Also fixes #2330 (folded in from #2337, second commit).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:36:53 +01:00
n30nexandn30nex 202bafea28 fix(observers): show metadata while analytics loads (#2339)
Partial follow-up to #1778, limited to observer details; other pages
remain separate.

Observer identity, status and radio information now render when the
basic request finishes. Analytics and clock calibration load into their
own regions, so either can finish first. An analytics failure preserves
usable metadata. Identity failures and obsolete responses cannot
overwrite the current view, and failed chart construction is cleaned up.

The same three requests start together. Generation ownership, request
deduplication, date-window behavior and disposal remain covered. No new
fetches, settings, dependencies or customizer options; secondary replies
preserve the actual metadata DOM node.

[RED
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40426343054/job/121932999182)
proves 14 new assertion failures while all 11 existing unit cases pass.
Local RED browsers reproduce 20 new failures while all 32 old cases
pass. GREEN `52eabad7` passes 221 standalone suites, 28 ownership unit
checks and 56 browser cases across desktop/mobile and both themes; the
parent independently repeated the 56 browser cases.

All three independent review roles passed. Held-response assertions
prove the rendering behavior without claiming a server-latency speedup.
[Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40427087948)
passes on this exact head, including all three browser shards, the
repeated slide-over stability gate and the architecture build.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-10-29 18:34:12 +01:00
n30nexandn30nex 1ed5e4ab3f fix(compare): exclude observer TX-only packets from overlap (#2346)
Red commit: `39295da8` ([intended CI assertion
failure](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40605770951/job/122544822813)).
Green commit: `d1bda496`.

Observer comparison counted an observer's TX-only packets as receptions,
producing false overlap. It now requests `excludeTx=true`: the selected
observer and non-TX direction must match the same observation in memory
and SQLite v2/v3. Normal packet lists and stored TX records remain
unchanged; missing directions retain legacy compatibility.

Refs #667. This completes a comparison slice, not the issue's remaining
observer counters or #671's fleet/SNR feature. Representative display
fields and existing time-filter semantics are unchanged. API/OpenAPI
docs describe the filter and multi-node bypass.

Validation:
- RED: Go tests compiled with 32 intended failures/16 control passes;
unit and browser regressions reproduced false overlap. CI reached the
new unit assertion.
- GREEN: all 48 packet cases pass, plus grouped-cache isolation and
OpenAPI tests. The added cache regression caught a malformed-radio
cache-key collision before correction.
- Full local server suite: 174s; ingestor: 185s; `npm run test:unit`:
pass. Final focused packet/grouped/OpenAPI checks: pass.
- E2E assertion added to
`tests/e2e/test-issue-1640-compare-discovery-e2e.js`; all 10 checks
pass, including correct 1/2 reception totals and 50% overlap.

Performance: still two capped bulk requests, indexed candidate lookups
retained, one direction check within the existing observation scan/SQL
predicate, no new persistent structures. No configuration/customizer
changes.

Final-head CI passed; independent reviews found no must-fix items.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-10-29 18:34:06 +01:00
n30nexandn30nex c211ef7643 feat(nodes): show relay activity from certain forwarding evidence (#2336)
Fixes #2331 with the additive API option.

The Last Relayed row shows green **actively relaying** only when
`last_certain_relay` falls within the existing
`healthThresholds.relayActiveHours` window. Missing, invalid, future or
expired evidence adds no idle/alive claim. Existing `relay_active`,
attributed timestamps and counts retain their meanings.

The optional node list/detail field uses the existing certain-relay
path: an eligible observed frame, matching hop, unique prefix, no
retired namesake and existing signature checks. Originating an advert
does not count. Forwarding another node's advert can count, even with
zero non-advert attributions. Adverts with an unknown origin cannot
establish forwarding.

Evidence uses transmission `first_seen`, not late observation receipt
time, and follows node-membership eviction. Snapshots take one read lock
over requested keys, O(page size), with no packet scan or new database
query. Work per qualifying hop stays bounded. No new dependency or
setting; configuration documentation and OpenAPI explain the
distinction.

The test-first history includes assertion-failing RED commits and GREEN
fixes. The initial RED's nil-return compile stub is explicitly permitted
by `docs/agents/TDD.md`. Local native server/ingestor suites, focused
race checks, 809 frontend helper assertions and 70 browser cases pass.
Browser coverage includes both themes and full/side/tablet detail views.

[Review regression
RED](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40421464618/job/121916723339)
proves both malformed-origin failures. All three independent review
roles pass at `d1fe79cb`. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40422655728)
passes on that exact commit, including all three browser shards and the
two-architecture image check.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-10-29 18:33:59 +01:00
n30nex 1e1bfa0255 fix(ingestor): require unique hop prefixes for relay liveness (#2334)
Fixes #2329 (option 1: unique-prefix liveness).

The ingestor could select a likely relay through neighbour adjacency and
refresh its `last_seen` even when its prefix matched two nodes.
Contextual resolution remains available in stored `resolved_path`;
liveness now uses the existing unique-prefix resolver, sharing the
parsed path and prefix-index snapshot. The current flood-frame
eligibility gate remains unchanged. Retired-prefix catalogue parity with
the server is separate.

The native SQLite regression exercises real `InsertTransmission`: the
displayed path still selects the adjacent candidate, neither colliding
relay is touched, and the unique preceding hop is refreshed. Existing
direct/TRACE/flood checks remain intact. [RED
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40407478987/job/121868990704)
fails the timestamp/touch-count assertions; [GREEN race
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40409860054/job/121877671472)
passes at `77be50e8`.

Full local server and ingestor suites, repeated focused race checks and
all 221 frontend suites pass. Existing test assertions are unchanged;
one misleading resolver comment is corrected. Frame/path assumptions
were checked against MeshCore firmware revision `81dbae59`.

Performance: one additional indexed lookup per hop, O(h), with no node
scan or extra database query. No schema, dependency, configuration or
customizer change. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40409860054)
passes on `77be50e8`, including the race detector, all browser shards
and architecture builds. Three independent reviews found no blockers;
final repository checks pass. Ready for maintainer review.
2026-10-29 18:33:53 +01:00
n30nex f9b7c66a8b fix(ingestor): open buffered writes before startup packet retention (#2328)
Partial fix for #2325; leave the issue open for the remaining startup
work.

MQTT subscribed early but its buffer could not drain until the entire
packet-retention backlog finished. Required startup work and the
migration barrier still finish first; ingestion now opens before the
initial packet sweep, so buffered writes can use the gaps between the
existing 250-transmission transactions.

The sweep remains synchronous with its count/error logging. No new
worker, shutdown owner, setting, retention window or batch size. The
readiness log reflects the actual gate opening. Earlier non-packet
maintenance, initial vacuum ordering and diagnostics publication during
maintenance remain unchanged.

Native SQLite tests exercise blocked migrations, inter-batch writes,
FIFO/drop accounting, stopped buffers, callback counts, rollback/retry
and main-function wiring. Removing the migration wait fails the
regression. Existing tests are unchanged; GREEN strengthens the new RED
test with a blocked-gate assertion and early-failure cleanup.

[RED
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40308382930)
fails the intended assertions. [Final-head
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40311222867)
passes on `f8a4d175`, including Go/JS tests, race checks, all browser
shards and architecture builds. Full local ingestor tests, repeated
focused race checks and local browser/API smoke checks pass. Three
independent reviews found no blockers in this partial scope.

This removes the whole-sweep startup gate; it does not bound an
individual deletion transaction or claim a measured live latency
improvement.
2026-10-29 18:33:47 +01:00
n30nexandn30nex ef17d78909 docs(api): document node resolution and battery history (#2338)
Partial follow-up to #1670; the broader documentation issue stays open.

Documents the existing node-prefix resolution and node battery-history
GET APIs: parameters, normalization and lookup rules, defaults,
ambiguity/visibility, ordered samples, nullable latest values and error
media types. The generator now has concrete success schemas for both.
Error statuses remain documented in prose under its existing convention.

Removes exactly these two entries from the known-gap list: 18 → 16, with
115 of 131 routes described. No handler, query, configuration or
dependency changes.

[RED
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40426035729/job/121931981058)
reaches the missing-documentation assertions. GREEN `521fb3ef` passes
both full native CGO suites and focused OpenAPI/real-handler contract
tests. Parent browser acceptance passed 28 checks through the rendered
Swagger UI and actual endpoints, including defaults, errors and
empty/null history.

All three independent review roles passed. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40427081025)
passes on this exact head, including all browser shards and the
two-architecture image check.

---------

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-10-29 18:33:41 +01:00
n30nexandn30nex 8437d22356 docs: use consistent snapshots for backup and restore (#2345)
The deployment guide copied a running `meshcore.db`, which can omit
committed WAL data or produce an inconsistent backup. It now uses the
existing authenticated `/api/backup` snapshot, checks the download
before publishing it, and explains how to restore with all database
users stopped while preserving the old database and sidecars for
rollback.

Fixes #703, following the [maintainer's remaining documentation
scope](https://github.com/Kpa-clawbot/CoreScope/issues/703#issuecomment-6163256620).

- Protects API credentials in a header file and keeps incomplete
downloads out of the completed backup set.
- Links the separate account-backup guide; configuration and secrets
remain separate backup responsibilities.
- Replaces unsafe live-copy scheduling instructions with the same
checked download procedure.

Validation:
- Exact shell example with real curl/SQLite: valid download, HTTP
401/500, empty, HTML, corrupt database, and truncated transfer. Failures
preserved the previous backup and published no new `.db`.
- Existing `TestBackup*` passed. Chromium downloaded an authenticated
snapshot from a disposable instance; a row committed only in its
nonempty WAL survived and `quick_check` returned `ok`.
- Disposable stopped-file restore and rollback recovered the original
WAL-only row and preserved the original files byte-for-byte.
- `git diff --check` and privacy preflight passed.

TDD exemption: pure docs; no application behavior changed and no test
files modified. No new config/customizer settings. Live operator
services were not used for validation.

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
2026-10-29 18:33:14 +01:00
efitenandClaude Opus 5.5 15e2d2a4a7 fix(packets): read HB from the row's own raw_hex header (#2351)
## Problem

The HB column on the packets page can show `4`. Path mode 3 (hash size
4) is reserved: `Packet::isValidPathLen` in `src/Packet.cpp` returns
false for it, and `Dispatcher::checkRecv` drops such a packet. So HB 4
is never a real value.

The three HB cells in `public/packets.js` found `path_len` with
`getPathLenOffset(p.route_type)`, the route type of the
**transmission**. The raw_hex in a child row (and in a live-updated row)
is an **observation's** raw_hex. The packet hash does not cover the
route bits, so the same packet is heard as FLOOD by one observer and as
TRANSPORT_FLOOD by another, and the transmission keeps the route type of
its first observation.

Example from a production DB (transmission 3645334):

| source | raw_hex start | path_len at |
|---|---|---|
| transmission | `01 12 3A646A…` (FLOOD) | byte 1 |
| observation | `00 E2E0 0000 1D 3A646A…` (TRANSPORT_FLOOD) | byte 5 |

Read at the FLOOD offset, the observation gives `0xE2`, `0xE2 >> 6 = 3`,
`+ 1 = 4`. In the last 300,000 observations of that DB, about 4,300 have
a header route type that differs from their transmission's.

## Fix

`pathHashSize(rawHex)` in `public/app.js` already takes the route type
from the header byte of the same raw_hex, and the detail pane uses it.
The three HB cells now call it through a small `hashBytesText` helper
instead of carrying their own copy of the offset logic.

Behaviour change: a raw_hex with no hash size (reserved mode 3, a DIRECT
packet with no hops, missing raw_hex) shows the same placeholder as
TRACE rows instead of a number. Before, missing raw_hex showed `1`.

## Tests

- New `tests/unit/test-hb-column-header-route.js` (7 cases, registered
in `test-all.sh`): transport raw_hex on a FLOOD transmission gives 1,
flood raw_hex with 2-byte hashes on a TRANSPORT_FLOOD transmission gives
2, reserved mode 3 never gives 4, for the helper, the flat row and the
group header. It fails on `upstream/master` with `'4' !== '1'`.
- `sh test-all.sh`: all standalone frontend suites pass.
- Browser: local server on a migrated copy of the E2E fixture, with one
observation rewritten to a TRANSPORT_FLOOD raw_hex. The expanded group
shows HB 2 on all three rows. The old code reads `0xE2` on that child
and shows 4.

## Performance

Same per-cell work as before: one header parse and one byte parse per
rendered row, O(1), no new allocations beyond the existing regex test in
`pathHashSize`. Only visible rows are rendered.

## Not done

- The child-row call site is not hit by a unit test: child rows only
render when the group is in `expandedHashes`, which the test API does
not expose. It is covered by the helper test and the browser check
above.
- The HB sort accessor (`hash_byte_count` / `hash_size`, sort key `hb`
in `packets.js`) is unchanged; it does not read raw_hex.
- `transportBadge(c.route_type)` on child rows still follows the stored
route type, not the observation header. Left out of scope.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 06:42:51 +01:00
efitenandClaude Opus 5.5 b65412ab83 feat(observer-logins): turn on neighbour reports in the firmware block of the credentials mail (#2349)
The firmware block of the observer credentials mail now ends with `set
mqtt.neighbors on`, so firmware observers send the same `/neighbors`
report that the OpenHop block already turns on ("Publish Neighbours",
#2347). The ingestor reads that report for region scopes (#1865).

## Changes

- `cmd/server/observer_mail.go`: one extra line in the firmware block,
and one sentence in its note. The sentence says the command sends the
neighbour table and region scopes once a day, and that older firmware
does not know it and should skip it.
- `cmd/server/observer_mail_test.go`: the full firmware block assertion
includes the new line, and the slot test checks the line and the note.
- `docs/observer-mqtt-logins.md`: the mail section describes the new
line.

## Tests

- `go test ./...` in `cmd/server`: ok. Skipped
`TestSaveGeoFilterPreservesFileMode`, which needs Unix file modes; this
run was on Windows.
- `TestObserverCredentialsMail*` failed before the change, on the
missing line, and passes after it.

## Not checked

- The command name and the 24-hour default come from #1865. I did not
read the observer firmware source.
- The wording of the error older firmware gives for an unknown command
is not quoted in the mail. The note only says to skip the line.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 06:25:42 +01:00
efitenandClaude Opus 5.5 ddce76a685 ci(e2e): run the E2E shards on ubuntu-24.04 and fail on a broken browser install (#2350)
`ubuntu-latest` now hands out `ubuntu-26.04` runners. Every E2E shard
that lands on one fails before a single test runs:

```
browserType.launch: Executable doesn't exist at /home/runner/.cache/ms-playwright/chromium_headless_shell-1208/...
```

The step "Install Playwright browser" runs `npx playwright install
chromium 2>/dev/null || true`, so its own failure is invisible and the
job only breaks at the first browser launch.

## Evidence

- Run 40779894898 (PR #2349, a mail-text change): three attempts, each
failing on `Executable doesn't exist`. The job log shows `Image:
ubuntu-26.04`, OS `26.04.1 LTS`.
- The master run at 2026-10-28 21:31Z passed. Its shard log shows
`Image: ubuntu-24.04`.
- `playwright` in package-lock.json is 1.58.2.

## Change

- `e2e-shard` gets `runs-on: ubuntu-24.04`, with a comment saying when
to unpin. The other jobs do not install Playwright and stay on
`ubuntu-latest`.
- "Install Playwright browser" no longer has `2>/dev/null || true`, so a
failed install fails at that step.

## Not done

- I did not find out why the install fails on 26.04: its stderr was
discarded. Once this is merged, a run with the pin removed would show
it.
- No Playwright upgrade. That is a separate change, and it may make the
pin unnecessary.
- Before this change, `install-deps` errors were also swallowed. If it
fails on 24.04, this PR's own CI will show it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 00:07:30 +01:00
efitenandClaude Opus 5.5 cadc666bd9 feat(observer-logins): OpenHop block in the credentials mail (#2347)
The observer credentials mail (#2306) had blocks for meshcoretomqtt, the
Gessaman/Dutch-MeshCore firmware, Home Assistant and plain values. An
operator running OpenHop (repeater software with its own MQTT uploader)
could not tell which of these matched its broker form, and picked the
format by trial.

## Changes

- `cmd/server/observer_mail.go`: a fifth block, "4. OpenHop (repeater
software)", before the plain values. It uses the field and option labels
of the OpenHop broker form:
- `Format: Letsmesh`, `Transport: TCP` or `Websockets` (from
`observerLogins.transport`), host, port, username, password, `TLS:
on/off`, `Use JWT Auth: off`, `Retain Status: on`, `Publish Neighbours:
on`.
- The note says Meshcoretomqtt works too, but the format labelled
"openHop" (value `mqtt`) does not: it publishes to
`meshcore/repeater/<name>`, which a personal login may not use.
- Publish Neighbours is on because the ingestor reads the `neighbors`
topic for region scopes (#1865).
- `openapi.go` and `docs/observer-mqtt-logins.md`: "four formats"
becomes five.

## Sources

Read from OpenHop, not tested on a device by me:
- format family and topics:
`openhop_repeater/repeater/data_acquisition/mqtt_handler.py`
(`MC2MQTT_FORMATS = ("meshcoretomqtt", "letsmesh", "waev")`, the `mqtt`
format's `meshcore/repeater/<node_name>` base topic, transport values
`tcp`/`websockets`);
- labels: the built web UI (`Configuration-*.js`: "Add Broker", option
labels `Letsmesh`, `Meshcoretomqtt`, `openHop`, `TCP`, `Websockets`,
"Retain Status", "Publish Neighbours", "Use JWT Auth").

An operator on our instance set it up from a screenshot of that form
with format Letsmesh, TCP, Retain Status and Publish Neighbours on; the
observer publishes, and its `neighbors` report set `configured_scope` on
its node.

## Tests

- `TestObserverCredentialsMailOpenHopBlock`: the block text for TCP
without TLS and for Websockets with TLS, the "openHop" warning, HTML
escaping, and the block order.
- `TestObserverCredentialsMailBlockTitles`: five titles in order.
- `cmd/server` suite green locally (the two Windows-only failures
skipped, as on master).

## Not in this PR

- The websocket path: OpenHop uses paho's default, the doc already tells
operators to match the listener path.
- Anything with the other fields of the `neighbors` report (SNR,
heard_secs_ago, default_scope): we are measuring how many observers send
it first.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:31:43 +01:00
n30nex 3bf678709f fix(ui): preserve navigation and menu focus during delayed updates (#2333)
Fixes #2332. Fixes #2335.

Two delayed UI updates could undo user actions. Rebuilding the More menu
removed its focused link; a late packet-list response could replace a
newly requested Nodes URL before `hashchange` ran.

The menu now preserves the selected destination when it stays in More or
moves inline, closes an empty menu, and leaves other controls' focus
alone. Packet URL updates now require the packets route, including its
existing detail/query forms. Observation selection and encoded filters
remain supported.

The changes add one lookup across the existing 12 navigation links and a
URL ownership check. No packet loop, API request, dependency, setting or
customizer change is added.

[Packet
RED](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40409110743/job/121874676123)
fails four wrong-route assertions. [Menu
RED](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40409898375/job/121877663579)
fails the intended focused-destination assertion after the packet
correction removes the competing navigation failure. GREEN is
`57522143`.

Local combined validation passes all 221 standalone frontend suites, 60
navigation browser checks, 17 branding/route checks and 11
filter/deep-link checks. The packet regression controls delivery of a
real API response; substituting the original code still fails it.
Existing exact title assertions remain; diagnostics now report the
actual title. Backend source trees are unchanged from the tested
baseline.

[Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40410894023)
passes on `57522143`, including all three browser shards and both
architecture builds. Three independent source reviews found no blockers.
Final repository checks and after-push browser verification pass. Ready
for maintainer review.
2026-10-28 21:55:40 +01:00
efitenandClaude Opus 5.5 d29957fbdc feat(observer-logins): lock the shared login out of migrated observers (#2348)
Follow-up to #2306 / #2319. With personal observer logins, a migrated
observer still is not safe from the shared login: the hook answers
`ignore` for the shared login, so EMQX's built-in database lets it
publish under any observer's topic, including one whose owner has moved
to a personal login. This PR lets an operator close that per migrated
observer, before the shared login is removed altogether.

## Changes

**users.db v8.** `observer_bindings.confirmed_at`: the moment the
owner's personal login first published under that pubkey.
- An auto-bind (the owner's first publish) is confirmed at once.
- An admin assignment stays unconfirmed until the owner's login
publishes under that pubkey; that publish sets it.
- Reassigning to another user or unbinding clears it; reassigning to the
same owner keeps it.
- Migration: existing `auto` rows get `confirmed_at = bound_at`, `admin`
rows stay NULL.

**Config.** `userManagement.observerLogins.sharedUsernames` (default
empty): the broker's shared login names. A name starting with `obs_` is
refused at startup.

**Authz hook.** For a login in `sharedUsernames`, a publish under
`meshcore/<x>/<pubkey>/...` is denied when that pubkey's binding is
confirmed, whatever the number of topic segments (the ingestor
attributes `meshcore/<x>/<pubkey>` and longer topics to that pubkey).
Logged as `publish denied: user=<name> observer=<8 hex>: confirmed
owner`. Every other request from a shared login keeps `ignore`, so the
built-in database decides as before. Personal logins are unchanged.

An orphaned binding (owner account deleted) does not count as confirmed.

**Admin.** The observers table of the Observer logins tab gets a
Confirmed column; the bindings JSON gets `confirmedAt`.

**Docs.** `docs/observer-mqtt-logins.md`: "Locking the shared login out
of migrated observers" (what it denies, how to see `confirmedAt`, the
risk below). `config.example.json` has the key.

## Risk for operators

An observer that is confirmed but still runs a second uploader on the
shared login loses that second uploader once its name is in
`sharedUsernames`. The deny log line names the observer.

## Tests

- `internal/users`: v7 to v8 migration on a real v7 database, auto
confirm, admin assignment then owner confirm, non-owner and repeated
confirm, reassign clears, orphan reassign.
- `cmd/server`: the decision table (confirmed, unconfirmed, unbound,
non-publish), topics with 3, 4 and 5+ segments, trailing slash, empty
and `client` iata, lowercase key; owner publish confirms, confirm
failure still allows and retries; reassign, unbind and user removal
clear the flag; the index load against a real store; config validation;
the hook and admin endpoints end to end.
- `tests/e2e/test-user-management-e2e.js`: the Confirmed column for an
auto-bound observer.
- Local: `internal/users`, `cmd/server` (two Windows-only tests skipped,
as on master) and `cmd/ingestor` green. The E2E assertion was not run
locally; CI runs it.

## Not in this PR

- Using `confirmed_at` for verified `/neighbors` reports: a separate PR,
built on #2300.
- Removing the shared login: still an operator step after migration.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 19:10:41 +01:00
TeTeHackoandClaude Opus 5.5 660cbbf5ff deps: golang.org/x/net v0.61.0, x/crypto v0.58.0, paho.mqtt.golang v1.5.1; runtime alpine:3.24 (#2297)
A Trivy scan of the published image found **19 HIGH and 20 MEDIUM** in
Go dependencies, plus a runtime base past its support window. There is
no Dependabot here, so this collects the bumps in one PR.

| dependency | from | to | why |
|---|---|---|---|
| `golang.org/x/net` (ingestor) | v0.27.0 | v0.61.0 | CVE-2024-45338 and
later advisories, fixed up to v0.60.0 |
| `golang.org/x/crypto` (server, `internal/users`) | v0.31.0 | v0.58.0 |
CVE-2025-22869, CVE-2025-47913 and later; `x/sys` follows to v0.49.0 |
| `github.com/eclipse/paho.mqtt.golang` | v1.5.0 | v1.5.1 |
CVE-2025-10543, an integer overflow in the UTF-8 string encoding |
| runtime image | `alpine:3.20` | `alpine:3.24` | 3.20 is out of
support; its musl 1.2.5-r3 carries CVE-2026-107891 |

The Alpine change covers `Dockerfile`, `Dockerfile.go`, the arm64 smoke
step in `deploy.yml` and the comment in
`docker/test-select-service-user.sh`.

**What changes besides the versions:**

- The new `x/` modules declare `go 1.26.0`, so the `go` directive of
`cmd/server`, `cmd/ingestor` and `internal/users` moves up from 1.22.
The builder image and CI already run 1.27.
- `x/sync` follows paho to v0.17.0.

**What is left:** only `GO-2026-5932`. It flags `x/crypto/openpgp` as
unmaintained; nothing here imports that package and no fix exists.

## Checks

- Every module passes `go mod tidy`, `go build` and `go vet`.
- `go test -race` passes in `internal/users`, `cmd/ingestor` and
`cmd/server`.
- A local image build on alpine 3.24.3:
  - ships mosquitto 2.1.2, caddy v2.11.4 and supervisor 4.3.0
- keeps the caddy and mosquitto accounts at the same uid:gid (100:101,
101:102), so `test-select-service-user.sh` still describes the image
(116 assertions pass)
- comes up healthy, ingests an MQTT status through mosquitto and serves
the frontend

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:09:01 +01:00
TeTeHackoandClaude Opus 5.5 77a5f14925 feat(observers): radio setting per observation from the observer's radio history (#1869) (#2292)
Part of #1869.

## Problem

`observers.radio` keeps only the **last** `MHz,kHz,SF,CR` an observer
reported. Once an observer is retuned (say from SF7 to the EU narrow SF8
setting to listen to a neighbouring mesh), every packet it ever heard
reads as heard on the new setting, and there is no way to tell an SF7
packet from an SF8 one.

## What this does

As #1869 proposes, the packet data model is untouched: the setting of an
observation is resolved from its observer and time.

**Ingestor** (all schema in `dbschema.Apply`)

- `observer_radios`: one row per stretch of time an observer, and each
radio of a multi-radio node, stayed on one setting. Every live status
extends the current row and a change starts a new one; retained replays
never extend a row. On the first run it is seeded with every observer's
current radio, which is what the UI already showed for the past.
- Settings are stored canonical through `dbschema.CanonicalRadio`, so
`869.4320068,62.5,7,5` and `869.432,62.50,7,5` are one setting.
- `observation_radios` (`observation_id → rx_radio_id`): multi-radio
openHop nodes tag each packet with `rx_radio_id` and list `radios: [{id,
radio}]` in their status, so their observations resolve per radio.
- The table is sparse: single-radio nodes never send the field, so
`observations` and every query that scans it stay untouched.
  - The row id comes from the observation upsert via `RETURNING id`.
- Rows are pruned with their transmissions, and a purged observer takes
its history along.

**Server** (read-only)

- No `AssertReady` change: an absent table reads as empty and is probed
again later, the same rule as `advert_route_evidence`.
- The history is held in memory. The live poller checks it on every tick
(two primary-key lookups), so a retune or a new `rx_radio_id` is known
before the observation is broadcast. A full reload runs every 10 min.
- `radio` is added to observations, packets, grouped rows (taken from
the header observation) and the live feed.
- `GET /api/packets?radio=MHz,kHz,SF,CR` filters by setting.
- Combined with `observer=`, it means *that* observer heard it on that
setting.
- A radio query is served from memory instead of falling back to SQL,
which would return the window unfiltered.
- `GET /api/radios` (named struct) and `radio_history` on `GET
/api/observers/{id}`.

**Frontend**

- Packet filter fields `radio`, `freq`, `bw`, `sf`, `cr`, e.g. `sf == 8`
or `freq > 869.5 && bw == 62.5`.
- A Radio row in the packet detail and a Radio column in its
observations table (only when the server reports one).
- A radio history table on the observer page once there is more than one
stretch.
- `RfParams.radioText` formats the setting. It escapes all five
characters and returns non-numeric input as it came, escaped.

## Performance

`txMatchesRadio` runs once per observation inside `filterPackets`.

- Each observation costs one map lookup by observer id.
- Observers with a single stretch (nearly all) answer from that lookup
directly.
- Retuned observers add a binary search over their few stretches.
- Multi-radio observers add one lookup by observation id.
- The snapshot is loaded once per filter call.

`BenchmarkTxMatchesRadio`, 30K packets / 150K observations (20
observers, 2 of them retuned):

```
BenchmarkTxMatchesRadio-24    602    2056924 ns/op    0 B/op    0 allocs/op
```

Memory is one entry per stretch plus one per multi-radio observation;
nothing is added per observation otherwise.

## Tests

- **dbschema:** seed runs once, canonical spellings, the tables exist
without `observers.radio`.
- **ingestor:**
  - stretches extend and split; canonical spellings do not split
- two radios of one node hearing one transmission over two paths each
keep their own radio
  - a retained status does not write history
  - prune and purge
  - `rx_radio_id` reaches `PacketData`
- **server:**
- resolution before, between and after stretches, and with an unknown
radio id
  - `observer` + `radio`, and grouped rows carry `radio`
  - incremental refresh, `/api/radios`, `radio_history`, missing tables
  - the benchmark above
- **frontend:** the new filter fields in
`tests/unit/test-packet-filter.js`, and XSS cases through the real
`RfParams.radioText` and the history renderer.
- **E2E:** `tests/e2e/test-radio-setting-1869-e2e.js` checks the grouped
rows' radio, `sf ==` on the packets page, the detail Radio row and the
observer history. It is added to `scripts/non-unit-tests.json` and to
shard 2.

`go test -race` in `cmd/server`, `cmd/ingestor` and `internal/dbschema`
passes, along with `sh test-all.sh`, `eslint@8` (0 errors) and the XSS
diff gate. The new E2E test and `test-e2e-playwright.js` pass against
the fixture.

## Browser validation

This runs on a live instance with six observers, one of them retuned to
869.618/62.5/SF8/CR8:

- The packet detail shows `Radio: 869.432 MHz · SF7 · BW 62.5 kHz · CR
4/5`.
- `sf == 7` shows 106 of 106 grouped rows and `sf == 8` shows 0 of 107;
the SF8 observer has not heard anything yet.
- `/api/radios` lists both settings.
- No page errors.

## Notes

- **The customizer (rule 8)** gets nothing: the only constants are
server-side refresh intervals.
- **Attribution timing:** packets heard between a retune and the
observer's next status message (minutes) are attributed to the previous
setting, because a stretch starts when the status is handled. This is
documented at `RecordObserverRadios`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:08:40 +01:00
n30nex 643a53e98d fix(operators): refresh update metadata and wait for readiness (#2285)
Red commit: `7277ca23` ([failing
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40190626951)).

Fixes #2229. Management startup now defines logging helpers before
normalizing CRLF environment files. Updates refresh version, revision
and build time after checking out the target. Deployment verification
and all five Compose healthchecks use `/api/healthz`, so an available
statistics endpoint cannot report an unfinished startup as healthy.

- The actual-script regression passes 15 checks: LF/CRLF/no environment
file, repeat startup, all update targets, failed checkout,
delayed/failed readiness, HTTPS readiness and unchanged informational
statistics.
- RED CI reproduced all 11 intended failures; GREEN `7ff9050b` passes
the full standalone frontend suite and shell syntax checks.
- Browser verified: an isolated Go fixture at http://127.0.0.1:18851
renders Home and returns ready=true from `/api/healthz`.
- Git, Docker and network boundaries are mocked in management tests. No
real container update was performed locally; CI validates the
Compose/build path.
- No new configuration, dependencies, schema changes or
packet-processing work.

Privacy, whitespace and scope checks passed. The external home-directory
preflight bundle is unavailable; applicable checks were performed
locally. Independent review found no outstanding fixes. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40191647297)
passed on `7ff9050b`, including all three browser shards and both
architecture builds.
2026-10-26 15:50:36 +01:00
n30nex b5caea1d5f fix(analytics): render safe, correctly sized battery status icons (#2282)
Red commit: `4cc2efb3` ([failing
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40189921192)).

Fixes #2232. Battery badges now render their fixed status icon and
append voltage as text, including zero. Chart sizing excludes inline
icons, and status colors use the existing theme variables. Unknown
status values stay muted.

- The original browser regression failed on literal SVG; a second local
run with only the JavaScript fix failed on oversized icons.
- Validation on `eef0872e`: all standalone frontend suites; 47 focused
unit checks; 41 Chromium checks, including both themes, hostile voltage,
accessibility and 16×16 icon bounds.
- Browser verified: http://127.0.0.1:18851 on an isolated Go fixture.
- E2E assertion added: `tests/e2e/test-issue-1648-m2-icons-e2e.js:134`.
- Constant work for one badge; no new requests, dependencies, settings
or schema changes.

Privacy, whitespace, CSS-variable and repository XSS checks passed. The
external home-directory preflight bundle is unavailable; its applicable
checks were performed locally. Independent review found no outstanding
fixes. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40190620324)
passed on `eef0872e`, including all three browser shards and both
architecture builds.
2026-10-26 15:44:23 +01:00
n30nex d07186af43 fix(nodes): preserve names and metadata in preview panels (#2315)
Fixes #2309 and #2312. Partial follow-up to #1154 (U1 and tablet
timestamp parity).

Node previews omitted Last Relayed, Traffic share and Hash Prefix, while
tablet previews read `advert_name` instead of the API's `name`. Shared
rendering now presents those fields consistently across full detail,
side preview and tablet, with the existing timestamp tooltips.

Relay attribution is labelled neutrally: ambiguous relay evidence no
longer claims a node is alive. Existing certainty/status logic is
unchanged. Measured zero, legacy score fallback, role applicability,
missing values and escaped names/hash tooltips are covered. A resize
regression prevents duplicate score-row IDs across retained desktop and
tablet previews.

Only the selected node's metadata is formatted. Existing request counts
remain five for full/side and two for tablet; no new fetch, cache, hot
loop, setting, customizer value or dependency.

Validation: 806 helper checks, all 218 standalone suites, 46 browser
checks in both themes, eight health checks, and the main browser suite
(149 passes, three existing fixture skips). The unchanged backend tree
passed full server/ingestor suites. Independent reviews completed; the
parent verified the two-line follow-up from its resize check.

Test-only RED `e0f681ae` produced [eight expected assertion
failures](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40232409958/job/121286783705).
Follow-up `2bb1ccd` repairs the help fixture's scroll range without
relaxing its checks; all 13 help checks pass across three font/CPU
configurations. [Final-head CI
passes](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40234768485),
including all browser shards and architecture builds.
2026-10-26 15:44:10 +01:00
n30nex 6e1abf0663 ui(nodes): identify path links as example packets (#2296)
Red commit: `79abbba0` ([regression
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40203075654)).

Fixes #2291; addresses U7 of #1154. The two observed-path links now say
View example packet →, accurately describing their existing
sample-packet destination. Recent-advert labels and link behavior remain
unchanged.

- Production diff: two text literals in public/nodes.js.
- Existing browser suite preserves contrast/highlighting assertions and
adds exact labels, canonical destinations and real click-through from
side/full/mobile views.
- RED fails eight label assertions. GREEN passes 15/15 checks normally
and with 4× CPU slowdown; all 218 standalone frontend suites pass.
- Browser verified: http://127.0.0.1:18902.
- E2E assertion added:
tests/e2e/test-issue-1146-path-link-contrast-e2e.js. Initial loads and
theme toggles wait for actual theme-refresh readiness before capture,
fixing a reproduced screenshot-detachment race in the touched harness.
- No runtime logic, requests, settings, dependencies or performance
changes.

Independent reviews and local preflight checks passed. External
home-directory preflight bundle unavailable; applicable checks were
performed locally. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40204585697)
passed on `ae849cc6`, including all three browser shards and both
architecture builds.
2026-10-26 15:43:57 +01:00
n30nex fb101e46e0 fix(nav): fit primary links around late account controls (#2286)
Red commit: `e5589b4c` ([failing
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40191348162)).

Fixes #2216. Navigation measures the available flex strip instead of
guessing component widths. Optional statistics and spacing yield when
primary/active links would overlap More. Account insertion triggers a
layout event immediately, including while authentication is pending or
fails; normal user-change events retain their semantics.

- Regression CI reproduced link overlap across narrow widths and late
account controls. Review added a second RED test for the actual delayed
authentication bootstrap before the three-line correction.
- Validation on `f0ccbf4c`: full frontend suite; 89 user-management
checks; 56 navigation checks; existing priority and clipping suites.
Parent independently reran auth and browser checks.
- Browser verified: http://127.0.0.1:18854; real bootstrap overlap of
14.66px becomes 21.38px clearance. Resizing, account changes, keyboard
More and touch targets remain covered.
- E2E assertion added: `tests/e2e/test-nav-fluid-1055-e2e.js:89`.
- Work is bounded by the fixed navigation link set; events share one
animation frame. No polling, observers, new requests, dependencies or
settings.

Privacy, whitespace, CSS-variable, XSS and ESLint checks passed.
External home-directory preflight bundle unavailable; applicable checks
were performed locally. Independent review verified the bootstrap
correction with no outstanding findings. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40192737545)
passed on `f0ccbf4c`, including all three browser shards and both
architecture builds.
2026-10-26 15:43:43 +01:00
n30nex c853c64f1f feat(ui): identify current views in browser tab titles (#2287)
Red commit: `a62753de` ([failing
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40191439879)).

Fixes #2259. Browser titles now identify the current page, Analytics
tab, or selected node/packet while retaining the configured site name. A
Reach view reads `Node abababababab | Reach | CoreScope`. Detail
identifiers are limited to 12 characters; account/reset parameters stay
out of titles.

- Branding and page context have separate state, so delayed theme loads
and edits cannot erase the view or accumulate suffixes. Selection/close
paths that use replaceState also update the title.
- Validation: all standalone frontend suites; 29 focused unit checks; 16
browser steps covering routes, history, reload, tabs, real row clicks,
closing, branding and literal route text. An explicit All time selection
keeps the browser test independent of fixture age.
- Browser verified: http://127.0.0.1:18853 on an isolated Go fixture.
- E2E assertion added: `tests/e2e/test-customize-branding-e2e.js:48`.
- Fixed-size work at navigation/selection boundaries; no extra requests,
DOM observers, render-loop work, dependencies or settings.

Privacy, whitespace, XSS and ESLint checks passed. External
home-directory preflight bundle unavailable; applicable checks were
performed locally. Independent review found no outstanding fixes. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40193902834)
passed on `4c037db5`, including all three browser shards and both
architecture builds. The existing logo test now asserts the full
page-and-brand title; its 6/6 checks and the 16/16 title/branding checks
also pass locally.
2026-10-26 15:43:30 +01:00
TeTeHackoandClaude Opus 5.5 684f89f200 feat(frontend): serve Leaflet, leaflet.heat and Chart.js from vendor/, not unpkg.com (#2299)
Refs #1554.

`index.html`, `area-map.html` and `geofilter-builder.html` load Leaflet,
leaflet.heat and Chart.js from **unpkg.com** on every page view. That
causes three problems:

- **Privacy:** every visitor sends a request to a third party.
- **Availability:** where unpkg is blocked (script blockers, filtered
networks) or down, the map and every chart are gone. The observer page
then fails with `Error: Chart is not defined`.
- **#1554:** the CSP target there, `script-src 'self'`, blocks these
scripts just like the inline handlers the issue lists, but they are
missing from its inventory.

## Change

- **Vendored libraries:** `public/vendor/leaflet-1.9.4/` (with
`images/`), `vendor/leaflet-heat-0.2.0.js` and
`vendor/chart-4.5.1.umd.min.js`.
- They are the unpkg dist files byte for byte, verified against the SRI
hashes `index.html` already pins.
- The two standalone pages loaded Leaflet without SRI; they now carry
it.
  - This is the same pattern as `vendor/leaflet.markercluster.js`.
- **`.gitattributes`:** `public/vendor/** -text`. Otherwise `* text=auto
eol=lf` rewrites `leaflet.css`, which ships with CRLF, and the browser
refuses it for the integrity mismatch. I hit exactly that while
preparing this.
- **`scripts/instrument-frontend.sh`:** now copies `vendor/`
recursively. The flat `cp public/vendor/*` left the E2E server without
`vendor/leaflet-1.9.4/`, which showed up as `L is not defined`.
- **New test `tests/unit/test-vendored-libs.js`:**
  - no off-site `<script src>` or stylesheet in the served pages
  - every `integrity` attribute matches its vendored file
  - the images Leaflet's stylesheet references exist

  I checked that it fails against the old `index.html`.

## Checks

- `sh test-all.sh` passes.
- These E2E suites pass against the fixture: `test-e2e-playwright.js`,
`test-analytics-fluid-charts.js`, `test-issue-1236-map-mobile-e2e.js`
and `test-issue-1329-map-controls-accordion-e2e.js`.
- On a live instance, a headless browser with `unpkg.com`, `jsdelivr`
and `cdnjs` blocked:
  - the observer page renders all four charts
  - the map loads tiles and markers
  - `typeof L` and `typeof Chart` are defined
  - no CDN request is made and no page error occurs

## Not in this PR

Swagger UI on `/api/docs` (`cmd/server/openapi.go`) still loads
`swagger-ui-dist` from unpkg. I can vendor it in a follow-up if wanted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 15:43:16 +01:00
n30nex e61ff6e473 fix(observers): keep detail responses tied to the current view (#2323)
Fixes #2316 and #2320. Related to #1778; progressive loading remains
separate.

Delayed observer responses could replace a newer observer or period,
overwrite its error/status/calibration, and leave detached charts alive.
Each load now captures its observer and days and checks ownership before
success or failure can update the view. Leaving the page invalidates
pending loads, including same-observer revisits.

New visits also reset the request window to the selector's existing
seven-day default. Previously, a new page could display seven days while
requesting the preceding visit's 30-day window.

Production change: 12 added lines and three replacements in one frontend
file. No new requests, caches, settings or dependencies; the ownership
check is O(1). Existing request deduplication, comparison controls,
chart cleanup and current-request errors are preserved.

Validation: 11 unit checks, all 219 standalone suites, 32 controlled
browser cases across desktop/mobile and both themes, comparison 9/9, and
observer-IATA 5/5. Core browser run exited successfully with 149 passes
and three declared fixture skips; existing conditional subchecks remain.
The unchanged backend tree passed both full backend suites. The parent
independently reran the new unit/browser tests.

Test-only RED `a73c227f` produced [nine unit and 28 browser assertion
failures](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40239824064).
Three independent reviews passed. [Final-head
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40240673369)
passes on `ba52d873` integrated with upstream `11ce1235`, including all
browser shards and architecture builds.
2026-10-26 15:42:56 +01:00
n30nex 1dfcd0f5d3 fix(analytics): preserve distance invalidation during an active build (#2284)
Red commit: `38c0abe1` ([failing
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40190273036)).

Fixes #2281. Background-fill completion could reset an active
`sync.Once`, causing a fatal mutex unlock or letting an older distance
build mark an incomplete index ready. Build admission now uses the
existing mutex, and a generation check prevents stale completion from
consuming invalidation. The loader also releases the store lock before
acquiring the distance gate.

- Deterministic regression drives the actual loader, recomputer and HTTP
handler; the old runtime crash is contained in a child process.
- Two cold observations stay below the normal 5% rebuild threshold. Ten
concurrent callers must share one rebuild, and the final response must
contain the cold path.
- Validation on `b8a2333b`: full server suite, focused distance suite,
ten repeated race runs plus a parent race rerun. Shared-base ingestor
suite passed.
- Browser verified: http://127.0.0.1:18855 returned 202 with
Retry-After, then 200 with 20 paths and 20 hops.
- Constant-time admission/invalidation and one fixed-size counter;
existing computation costs remain unchanged. No schema, settings or
API-shape changes.

Privacy, whitespace and scope checks passed. The external home-directory
preflight bundle is unavailable; applicable checks were performed
locally. Independent review found no outstanding fixes. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40191246966)
passed on `b8a2333b`, including all three browser shards and both
architecture builds.
2026-10-26 15:42:42 +01:00
n30nex f66cd320da perf(graph): reuse indexed shortest-path workspaces (#2293)
Red commit: `8b3cffd0` ([regression
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40203043161)).

Fixes #2226. Bridge and coverage scoring now convert keys once and reuse
indexed distance/predecessor workspaces and at most one heap item per
vertex. Existing algorithms, normalization, snapshot publication and
synchronous startup behavior remain intact.

Five paired runs, deterministic 2,000-node/10,000-edge fixture, Go
1.27.2 on Windows/amd64, GOMAXPROCS=2:

| Calculation | Median time | Allocated bytes/run |
|---|---:|---:|
| Bridge | 8.237s → 0.819s | 2,083,295,328 → 2,517,664 |
| Coverage | 4.900s → 0.694s | 1,508,440,736 → 2,209,984 |

The desktop was shared; baseline bridge runs ranged 7.81–17.56s. These
are algorithm/allocation measurements, not startup/API latency or
retained memory. Rebuilding committed `5f34b5bf` produced the exact
measured binary hash.

- Independent path oracle, edge/epsilon/tie/disconnected cases,
allocation regression, race checks and full server/frontend suites pass.
- Browser verified: isolated native servers at http://127.0.0.1:18903
and http://127.0.0.1:18904 return identical nonzero graph scores.
- O(V+E) bounded workspace; O(V(V+E) log V) calculation. No persistent
cache, schema, dependency or configuration change.

Independent reviews and local preflight checks passed. External
home-directory preflight bundle unavailable; applicable checks were
performed locally. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40204557795)
passed on `5f34b5bf`, including all three browser shards and both
architecture builds.
2026-10-26 15:42:29 +01:00
n30nex 63d63ea90a perf(healthz): avoid scanning retained packets after startup (#2305)
Fixes #2301.

Readiness probes currently scan every retained transmission while
holding the packet-store read lock. Once `StartupLoadDone` closes, this
reads the maintained observation counter beside the transmission count
under that same lock. Count extraction becomes O(1), with no new cache,
query or allocation.

Startup retains the original scan: the background loader publishes
indexes before its packet slice, so switching on readiness or
`LoadComplete` would be premature. Response fields, readiness status,
ingestion and configuration remain unchanged.

Whole-handler benchmark medians, microseconds per probe:

| Retained transmissions | Upstream | Candidate |
| ---: | ---: | ---: |
| 1,000 | 72.14 | 83.84 |
| 30,000 | 320.85 | 65.74 |
| 100,000 | 1,228.50 | 81.97 |

Four alternating pairs, 10,000 iterations/size, real SQLite-loaded
fixtures with three observations per transmission on average; Go 1.27.2,
Windows/amd64, two CPUs. All samples retained; shared-host variability
means no small-cache or production-latency improvement is claimed.
Allocations remain 36/probe. The included benchmark supports
reproduction against unchanged upstream production plus the identical
harness.

TDD exemption: pure performance refactor; **all pre-existing test files
are byte-unchanged**. Added characterization tests pass before and
after, covering loaders, deduplication, live updates, eviction, partial
startup failures, concurrent snapshots and liveness. Independent review
prompted test-only follow-up `2a0083b1`, which detects premature counter
use; production remains identical to measured `1b8e6680`.

Full server, race, ingestor and post-push browser/API checks pass
locally. [Final-head CI
passes](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40213482779),
including all browser shards and architecture builds. No customizer
changes.
2026-10-26 15:42:15 +01:00
n30nex abad8a26b3 fix(liveness): require evidence from each observed relay frame (#2298)
Red commit: `46e3b082` ([regression
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40203680455)).

Fixes #2266 and #2267. Certain relay activity now requires an eligible
path from the observation that supplied it. A representative
transmission header, destination attribution or catalogue uniqueness
alone cannot supply that evidence. Later valid FLOOD evidence survives
observer/path deduplication.

The fix preserves valid own packets and excludes failed signatures,
planned DIRECT/TRACE paths, ambiguous/retired prefixes and missing or
mismatched headers. Firmware reference: meshcore-dev/MeshCore
`81dbae59`.

- One eligibility byte replaces an unused raw-frame field. StoreObs
remains 152 bytes in the protected 160-byte allocation class; no wire
frames are retained. Existing dedup maps gain one pointer per slot only
at 64+ observations.
- Processing is linear in retained paths, bounded by 64 wire path bytes.
No new SQL, schema, configuration or maps.
- Tests cover five load/ingest paths, late duplicates,
analytics/adaptive thresholds, positive controls and concurrent readers.
Review correction `4ad6c5ad` preserves lazy decoded caches while
retaining escaped/legacy signature rejection.
- Full server suite, focused/race checks and frontend checks pass.
Browser verified on http://127.0.0.1:18905: both upstream false-positive
cases disappear and a genuine later FLOOD is recognized.

Independent review findings are addressed. Local preflight checks
passed; external home-directory bundle unavailable. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40205015381)
passed on `4ad6c5ad`, including all three browser shards and both
architecture builds.
2026-10-26 15:42:01 +01:00
AdamandClaude Opus 5.5 d777f4b7ce fix(ingestor): only flood relay hops refresh nodes.last_seen (#2307) (#2308)
Fixes #2307.

The ingestor refreshed `nodes.last_seen` for every resolved hop in a
path. For DIRECT routes those hops are still to come, and for TRACE
`path_json` is the route the trace was asked to take, so tracing or
messaging a switched-off repeater kept it looking recently heard.

## What changes

**`cmd/ingestor/db.go`**
- New `pathHopsTransmitted(data)`: true only when
`PathBytesAreHops(payloadType)` and the route is FLOOD or
TRANSPORT_FLOOD. It's the same rule as the server's `relayHopHexLen` in
`certain_heard.go`.
- `InsertTransmission` calls `touchRelayNodesLocked` only when that
holds. `observations.resolved_path` is still written for every
observation, so analytics are unchanged.
- The route and payload type come from the observation's own decoded
header (`PacketData`), not the transmission's first frame (see #2266).

## Perf

There's one integer comparison per observation, before the existing
touch. The touch now skips DIRECT and TRACE observations, so it does
fewer `UPDATE nodes` calls, never more.

## Tests

`cmd/ingestor/relay_touch_test.go`:
`TestInsertTransmission_TouchesOnlyFloodRelays` goes through
`InsertTransmission` with a real store and prefix index, using a node
whose 2-byte prefix is unique:

- FLOOD and TRANSPORT_FLOOD TXT_MSG: `last_seen` advances to the rx time
- DIRECT TXT_MSG and DIRECT TRACE: `last_seen` is unchanged

With `db.go` from master, the DIRECT and TRACE cases fail.

How I checked:
- `go vet ./...` and `go test ./...` in `cmd/ingestor` pass, and `gofmt
-l` is clean (`golang:1.27` container, since there's no local Go
toolchain).
- No server, frontend or schema changes, so I didn't run the browser and
E2E steps.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 15:41:48 +01:00
n30nex 410db29fe6 chore: remove unused legacy Dockerfiles (#2314)
Fixes #2310. Partial follow-up to #1385 (PR-C).

Remove unused `Dockerfile.go` and `Dockerfile.node`, which describe
retired alternate builds. Current workflows, Makefile, Compose variants
and operator scripts already select the canonical `Dockerfile`.

Remove the obsolete formatter exclusions and label the old Node-to-Go
migration guide historical, linking current deployment instructions.
Historical release prose remains untouched. The canonical image, runtime
files, dependencies and Compose behavior are unchanged.

Validation: audited all 1,501 tracked files; old and new formatter
inputs are exactly the same 633 real Go files. Both formatter commands,
YAML/shell/workflow-reference checks, existing workflow/routing tests,
COPY gate and test inventory pass. All 218 standalone frontend suites
pass. The unchanged backend tree passed full server/ingestor suites, and
the unchanged app passed 12 browser smoke cases.

TDD exemption: behavior-preserving cleanup/refactor plus documentation;
no test files modified. `make` and `actionlint` were unavailable
locally; equivalent formatter and focused workflow checks ran directly.

[CI passed on this
head](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40230911566),
including canonical container builds and all browser shards. Three
independent reviews passed. The published historical guide and its
current-deployment links render correctly in Chromium. No
settings/customizer changes.
2026-10-26 15:41:34 +01:00
n30nex 3a85a98cc5 docs(api): describe the supported packet-list query parameters (#2294)
Red commit: `cee2a4bc` ([regression
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40203052113)).

Partial follow-up for #1670. GET /api/packets now documents all 16
supported query parameters, including hash, route, node(s), region,
channel, area and expand. It removes unsupported sort/timeRange/search
entries, corrects numeric types, and describes actual defaults, limits
and memory/SQL/multi-node differences.

This changes only packet-route metadata. Runtime API behavior and the
schema generator stay unchanged.

- Served-spec regressions fail on upstream while actual-route controls
pass.
- Focused OpenAPI/SwaggerUI tests, full server suite and full standalone
frontend suite pass on `d8014193`.
- Browser verified: http://127.0.0.1:18906/api/spec exposes the 16
supported parameters and integer types; the running packet route
confirms default limit 50 and offset 0.
- No new requests, dependencies, settings, schema or untyped-map
occurrences.

The umbrella remains open: current route coverage stays 98/122 with 24
allowlisted gaps. Other endpoint parameters/schemas and a generic
parameter-presence gate are outside this PR.

Independent reviews and local preflight checks passed. External
home-directory preflight bundle unavailable; applicable checks were
performed locally. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40204567683)
passed on `d8014193`, including all three browser shards and both
architecture builds.
2026-10-26 15:41:20 +01:00