Files
hermes-gadgetandgadgethd fa801dc004 fix: close ukmesh health and advert identity backlog gaps (#117)
* fix(backlog): close ukmesh health and advert identity gaps

* docs(review): record backlog round 2 pull request

---------

Co-authored-by: gadgethd <111318106+gadgethd@users.noreply.github.com>
2026-10-03 19:43:45 +01:00

58 KiB
Raw Permalink Blame History

UKMesh fix review — 2026-10-02

Branch: fix/ukmesh-burn-20261002. Base: 73ee004 (ukmesh-w5). Scope: the UKMesh burn brief, including the stretch queue. All edits and local verification stay in this worktree. No deployment, container restart, migration, live-tree access, or environment change is authorized or performed.

Baseline and evidence

  • Read knowledge.md, AI_MEMORY.md, and the full supplied brief, and consulted OpenViking. Tokensave tools and .tokensave/tokensave.db are absent here, so structural investigation uses scoped source searches and Git objects.
  • The initial worktree was clean. git symbolic-ref refs/remotes/origin/HEAD fails because the local symbolic ref is absent. git ls-remote --symref origin HEAD and gh repo view gadgethd/ukmesh --json defaultBranchRef,nameWithOwner both identify main (remote HEAD 74d3ff1). The requested integration base remains ukmesh-w5; changing it would change the brief's scope.
  • Dependencies were missing; npm ci in backend and frontend installed the committed lockfiles without changing them. Initial backend npm test after installation: 327/327 pass, 0 fail, 0 skipped.
  • The brief describes additional untracked live-checkout code. Neither runState.test.ts nor ownerLastHopPrewarm.ts exists on the initial branch. Git history contains the former and its full policy in d834ba8, and an earlier feed measurement implementation in 333f180. No live checkout was inspected or copied.
  • push.md is ignored but absent from this worktree and all Git history. An optional location question received no answer during the work; OpenViking supplies no copy. Its exact discipline cannot be verified. The authorized fallback is a clean, reviewed and tested private branch, a non-forced push to that branch only, and an exact remote-SHA check. No merge or deployment follows.
  • Issue numbers in the brief are not assumed to be GitHub issue IDs: gh issue view 438 --repo gadgethd/ukmesh finds no such issue. The supplied brief is the requirements source.

1. Unit suite — #536 / #437 / #301

Decision: option (c), restore the complete intended retired-workload policy, including the missing export and production callers, from the isolated prior fix d834ba8. The initial runState.ts exactly matches that commit's parent. This is not a dummy export to make a test load: migration 044 explicitly removed the path-history cache, API and worker; beginAnalysisRun must reject a new retired lease, and getAnalysisWorkloadStates must describe retirement while preserving historical failures for audit. Supported workloads and unexpected active retired runs continue to expose their actual status and errors.

Evidence: after restoring only the historical test and workloadPolicy.ts, cd backend && node --import tsx --test src/analysis/runState.test.ts fails with SyntaxError: ... does not provide an export named 'normalizeRetiredAnalysisState'. After restoring the source policy wiring, the same command passes 3/3, with no database needed. cd backend && npm test now passes 330/330, 0 fail, 0 skipped; cd backend && npm run typecheck passes. Final suite results appear below.

2. Owner last-hop prewarm — #438

The named file is absent from the integration base. Recreated a prewarmer around the existing ownerService.getOwnerLastHopStrength cache and wired it after owner-auth initialization, with lifecycle shutdown draining. This branch exposes one rolling seven-day result (ownerRepository.fetchLastHopStrength), not the live brief's two separate windows; no speculative window API was added.

Each pass enumerates active, verified owner grants, deduplicates nodes per owner, and refreshes at most two owner/node pairs concurrently. Optional OWNER_LAST_HOP_PREWARM_CONCURRENCY=1 selects sequential work; values above two remain capped at two. The next pass starts 30 minutes after completion, so cold passes cannot overlap. Start/end logs and every ten completed nodes report owners/nodes completed and total, refreshed and failed. A refresh warns at 20s while still in flight; failures are counted and do not stop the pass.

Forced warm refreshes replace groups in the inclusive latest-bucket query window and retain earlier history. Foreground reads and warm refreshes share in-flight work. Cache keys include all owned nodes because that exclusion set changes the last-hop query; warming a shared node for one owner must not serve another owner's exclusion scope.

Validation: cd backend && node --import tsx --test src/owner/ownerLastHopPrewarm.test.ts src/owner/ownerService.test.ts: 8/8 pass, covering the concurrency cap, progress/failure/owner accounting, single-flight, deduplication, an in-flight slow failure, shutdown admission, cache ownership scope and retry. cd backend && npm run typecheck passes; cd backend && npm test: 338/338 pass, 0 fail, 0 skipped. Production cold-pass time and DB load are not measured.

Continuation verification adds three scheduler regressions: a long pass cannot overlap and starts its delay only after completion, enumeration failures recover on the next scheduled pass, and shutdown during enumeration admits no refresh. cd backend && node --import tsx --test src/owner/ownerLastHopPrewarm.test.ts src/owner/ownerService.test.ts: 11/11 pass. No scheduler source change was needed; mocked clocks exercise the real scheduling and shutdown paths.

3. Heartbeat alert semantics — #543 / #507

Confirmed currentWorkers set worker="link" from MAX(itm_computed_at). Replaced that source with Redis meshcore:link:v3:worker_heartbeat. The current worker's independent thread writes Unix seconds every 10s with a 45s TTL (the brief's 39s is a live observation, not the source's configured TTL). last_activity_at still reports ITM write activity separately.

The Prometheus gauge collects Redis on each scrape, rather than freezing the age until the five-minute health pass. Reads time out at 2s; missing, invalid, excessively future or unreadable timestamps publish -1, replacing any earlier healthy value. The queue alert now handles < 0 as well as > 180, with the existing three-minute holdoff. No threshold increase is needed for a genuine ten-second heartbeat. Empty queues do not trigger this alert.

Validation: cd backend && node --import tsx --test src/health/workerHeartbeat.test.ts: 5/5 pass, including the real Redis key, timestamp validation, expiry/failure, bounded timeout, repeated real metric scrapes and recovery. npm run typecheck passes; npm test: 343/343 pass, 0 fail, 0 skipped. .ukmesh-tools/promtool check rules logging/rules/meshcore.yml: 24 rules valid. .ukmesh-tools/promtool test rules logging/rules/meshcore.test.yml: 7 scenario groups pass, including batch gaps, missing heartbeats, holdoff, recovery, idle queues and existing stale cases.

Standalone promtool 3.15.0 was downloaded from the official release listing into an ignored worktree-local tools directory, without starting a container. Archive SHA256: 2a542df32eac02ee17b9d844fb2aa1de00dafa5476579ba8a3ba862e9d572ea0, matching the official listing. Live metric/alert behavior remains deploy-gated.

Publisher-side verification now exercises the actual link_queue_v3.start_worker_heartbeat thread with recording Redis clients: immediate publication, integer Unix seconds, ten-second refresh requests, 45-second expiry requests, failed-write reconnect, client-creation recovery, an already-stopped worker and interruption of a real ten-second wait on shutdown. The five tests use controlled waits for retry/cadence checks and a real event for shutdown; no Redis service or production client is accessed. Exact command: PYTHONPATH=viewshed-worker python3 -m unittest discover -s viewshed-worker/tests -p test_link_worker_heartbeat.py -v: 5/5 pass. This verifies the publisher's Redis command contract, not live Redis expiry or production alert behavior. The worker implementation is unchanged.

4. UK feed virtualizer — #303

Implemented option (a): natural border-box measurements from ResizeObserver, identity-keyed height storage, prefix offsets and a binary visible-range lookup. The previous 76px * index arithmetic placed spacers according to an assumed height even when metadata wrapped. The 76px estimate now applies only to unseen rows; rendered rows use their actual fractional-pixel heights.

Stable packet/scope identities retain measurements across filtering and prepends. Width changes invalidate offscreen measurements. Layout measurement and scroll anchoring preserve the visible packet through observer enrichment and new arrivals; a feed at the top remains pinned to live traffic. Both the desktop internal scroller and mobile document scrolling are supported. Details remain outside the packet list.

Validation: cd frontend && npm test: 99/99 pass, including 2/2 new offset/range tests. cd frontend && PLAYWRIGHT_PORT_BASE=4273 npx playwright test feed-virtualizer.spec.ts --project=public-desktop: 2/2 pass in Chromium, at 390px and 1280px, checking variable heights, contiguous rows and spacers, scroll-anchor preservation, enrichment, prepends, resizing, bottom reachability, filter reset, empty-filter recovery, new arrivals pinned at the top and absence of page errors. Test-only isolated ports avoid borrowing other sessions' servers; default ports are unchanged. The in-app Browser runtime could not connect to its trusted Node service, so these checks use the repository's Playwright CLI. cd frontend && npm run build passes with the existing large-chunk warning. The complete fixture-based E2E matrix passes 55/55, with no retries; production traffic is not tested.

cd frontend && npm run lint:css reports 6 pre-existing duplicate selectors in unchanged globals.css, map-app.css and owner-portal.css. Their diff against 73ee004 is empty; this change edits no CSS files.

Continuation adds empty-filter/repopulation and top-pinned arrival assertions at both widths. Each arrival has a distinct packet identity. Only the initial row readiness wait increases from 5s to the existing application's 15s bootstrap budget; row geometry and scroll-anchor assertions retain their previous limits.

Browser verification also uncovered incomplete test-server isolation: mobile and accessibility tests still navigated to hardcoded ports 4173–4175 even when the config selected another port range. They now share the config's origins. An explicit PLAYWRIGHT_PORT_BASE refuses to borrow existing servers. The three Vite test servers also receive separate dependency caches per port range and site role through a test-only config; the production Vite config is unchanged. Installed Vite source confirms optimization replaces the shared dependency cache; concurrent replacement is a source-based explanation for the earlier dynamic module fetch failure, rather than a proven production cause.

The first full run passed 53/55; both failures passed unchanged in isolation. After cache isolation, a run passed 54/55 with the feed's 5s initial readiness wait expiring before geometry assertions. The expanded feed tests and 15s readiness wait then passed the full 55/55 matrix without retries. A subsequent fresh-cache four-case check passed both feed widths and tablet topology but the mobile map-mode test exceeded its 30s total budget. Its unchanged isolated trace passed in 19.3s and shows two 4,600-node map bootstraps plus an accessibility scan. That test now has a scoped 45s total budget; its functional assertions are unchanged. The final fresh-cache check on ports 4303–4305 passes 4/4, without retries. These results establish local fixture behavior, not production performance or the absence of every intermittent browser failure.

Stretch queue

#424 — Compose adoption guard

The replacement script previously trusted Compose's selected container without checking its adoption metadata. A mocked backend with missing labels reached replacement: before the guard, TMPDIR="$PWD/.ukmesh-tools/tmp" bash scripts/test-replace-container.sh failed with adoption_empty_stops_before_mutation: expected Compose adoption rejection (65), got 0. This reproduces the config gap locally; it is not a claim about current live Docker state.

Added a read-only scripts/check-compose-adoption.sh SERVICE preflight. It requires exactly one current container with matching project, service, physical working directory and a config-file list containing this checkout's docker-compose.yml. replace-container.sh checks the requested service and backend before signature checks, pulls, receipts, migration runs or Compose replacement. Gaps stop with exit 65 and an operator-reconciliation message. No automatic adoption or network recreation is attempted. Operators can run the preflight separately when reviewing a deployment; this session runs it only through mocked Docker.

Validation: bash -n scripts/check-compose-adoption.sh scripts/replace-container.sh scripts/test-replace-container.sh passes. TMPDIR="$PWD/.ukmesh-tools/tmp" bash scripts/test-replace-container.sh: 6/6 drills pass (existing rollback and compatibility, plus empty labels, missing config-file label, wrong directory and wrong project). Gap cases assert no pull, migration/replacement or release receipt. Live adoption and operator reconciliation remain unverified and deploy-gated.

Continuation expands this to 16/16 mocked drills: wrong-service labels, absent/multiple containers, null/malformed labels, overlay-only config rejection, base-plus-overlay acceptance, and separate frontend/backend preflights. Every adoption rejection also asserts that image signature verification has not begun. A 10,000-service fixture reproduced an additional bug: config --services | grep -Fxq under pipefail rejected an existing backend with exit 65 when the producer hit SIGPIPE. The check now consumes the entire list with grep -Fx redirected to /dev/null. The same large-list drill passes after the fix.

#447 — Archive/delete inactive nodes of every role

The existing cleanup selected only (role IS NULL OR role = 2) and required an old, non-null last_mqtt_observer_seen_at, so roles 1/3 and never-bridged nodes could not enter its archive/delete path. Added a separate cleanupInactiveNodes pass to the health worker's existing initial/six-hour maintenance schedule. The original observer policy remains separate.

Inactive selection considers the latest node, MQTT, path, predicted-online and creation timestamps, and retains nodes with recent network or observer sightings. Source inspection found packet ingestion updates network sightings independently of the source node's main timestamp; those rollups therefore also fence deletion. The threshold stays bounded to 30–365 days. Test-network nodes, recent nodes and records with wholly unknown age remain protected. No prior bridge or role filter is required.

The existing transaction/advisory lock archives complete node and visibility records into maintenance_removed_records before any delete, then removes the two visibility sets and nodes together. Any failure rolls everything back. Authentication and packet-history tables are outside the cleanup. No schema or migration change is needed.

Validation: cd backend && node --import tsx --test src/maintenance/staleMqttObservers.test.ts: 6/6 pass. cd backend && npm run typecheck passes. Supplementary isolated PostgreSQL 18.3 / PGlite 0.5.8 tests execute the actual selection, advisory lock, archive and delete SQL against minimal in-memory fixture tables. They verify roles 1/3, never-bridged nodes, retention by each freshness source, test/unknown-age exclusions, archive readback, preserved auth/history fixtures, and real transaction rollback on injected archive and node-delete errors: 3/3 pass, 0 skipped.

Reproduce without a server or repository dependency change, from the worktree root:

npm install --prefix .ukmesh-tools/pglite --no-package-lock --ignore-scripts @electric-sql/pglite@0.5.8
cd backend
TEST_NODE_CLEANUP_PGLITE_MODULE="file://$PWD/../.ukmesh-tools/pglite/node_modules/@electric-sql/pglite/dist/index.js" node --import tsx --test src/maintenance/staleMqttObservers.integration.test.ts

The optional fixture test is excluded by the normal unit command. It does not apply project migrations or exercise TimescaleDB, the complete production schema, concurrent ingestion, production candidate counts or cleanup cost; those remain unverified. The final-stretch fixtures below expand the original single privacy function to the current node privacy/generation trigger chain and packet-path classifiers, extracted from the existing migration definitions.

Continuation found a real privacy interaction omitted by the initial fixture. The current sync_private_node_prefixes() definition in migration 049 treats deleting a private node as a privacy transition, removes its prefixes and makes historical packets public. Isolated PostgreSQL tests execute that exact function against fixture tables (no application migration or server) and reproduced is_private: true -> false, visibility_ok: false -> true for both cleanup policies. Both selectors now retain names containing the privacy marker or identities present in private_node_prefixes, including mismatched name/index states. Age does not revoke the user's privacy choice. Public inactive nodes of all roles and bridge states still take the archive/delete path.

The SQL fixture now has 7/7 pass, including the actual privacy trigger for both policies, the exact 30-day boundary, idempotent repeat passes, archive readback and transaction rollback. The optional integration command above is unchanged; cd backend && node --import tsx --test src/maintenance/staleMqttObservers.test.ts remains 6/6 pass. Removing a private identity while retaining its privacy decision requires a separate tombstone design; no schema change is introduced in this constrained branch.

Other heartbeat proxy signals — alert-semantics follow-up findings

Scoped audit command: rg -n 'worker_heartbeat_age_seconds|worker_heartbeat_timestamp_seconds|setHeartbeatAge' backend/src viewshed-worker logging. docker-compose.yml:811 mounts logging/rules into Prometheus's rules directory; logging/prometheus.yml:5 loads /etc/prometheus/rules/meshcore.yml. The edited rule is therefore the repository's active Compose rule source; deployed mounts were not inspected.

Signal Source and cadence Alert consumer / finding
worker="link" age Previously MAX(node_links.itm_computed_at); now the independent Redis heartbeat, collected per scrape. ActiveQueueWorkerHeartbeatStale fixed in this branch; DB write activity remains separate.
worker="health" age currentWorkers reads the previous MAX(worker_health_snapshots.ts) before captureWorkerHealthSnapshot writes the current snapshot (backend/src/health/status.ts:378, :519). backend/src/workers/health.ts:9 schedules the next capture five minutes after completion. HealthWorkerHeartbeatStale still uses >180 or <0 for 3m (logging/rules/meshcore.yml:126). A normal previous-snapshot age is about 300s and remains frozen between captures. Source-based inference: another false-positive candidate. Follow up with a genuine health-process heartbeat and producer/job scoping, plus healthy-cadence regression tests. No live firing frequency is claimed.
worker="path_learning" age MAX(path_model_calibration.updated_at) from published calibration (status.ts:377, path-learning/rebuild.ts:440); default rebuild cadence one hour (workers/path-learning.ts:7). No current rule consumes this label. It describes publication freshness, not process liveness; keep those concepts separate before adding alerts.
worker="link_backfill" age MAX(node_links.last_observed) (status.ts:385), the newest observation. No current rule consumes this label. Observation recency cannot prove a backfill process is alive or making progress.
Python meshcore_worker_heartbeat_timestamp_seconds Set by viewshed-worker/worker_metrics.py:58. Link updates are on the main loop (worker.py:2516); the Redis thread is independent (link_queue_v3.py:587). Viewshed's independent thread also updates its exported heartbeat (worker.py:2483). No current rule consumes the timestamp metric. It is unsuitable as a ten-second link-heartbeat substitute without changing its producer cadence. WorkerMetricsDown checks exporter up, which proves reachability rather than work-loop progress.

Queue oldest-job age and backup/restore receipt age describe backlog/freshness explicitly, rather than being renamed as liveness. The health-worker issue and the two unused proxy labels are filed here for follow-up, as requested; this branch changes only the link heartbeat semantics.

Final-stretch verification

Revalidated the clean, pushed continuation HEAD 68d018a before further work. Fresh local runs pass 350/350 backend units, 99/99 frontend units and 7/7 isolated cleanup SQL fixtures, with no failures or skips. The replacement drills remain 16/16, and all 24 Prometheus rules remain valid.

Production scrape labels uncovered another link-alert false-positive path: and on() paired every nonempty queue with any stale heartbeat across all reporters. Three new Prometheus scenarios failed before the fix: a backend Redis failure paged for a healthy health-worker queue, an idle stale reporter paged for a healthy same-job instance, and one stale reporter paged for both queues. The rule now matches job and instance, keeping queue evidence and heartbeat evidence from the same exporter. The threshold and holdoff are unchanged. .ukmesh-tools/promtool test rules logging/rules/meshcore.test.yml passes all 10 scenario groups, including scoped true-stale alert holdoff and recovery. .ukmesh-tools/promtool check rules logging/rules/meshcore.yml passes 24 rules. This reproduces rule semantics locally, not live alert frequency.

Expanded both feed browser cases to switch across the 860px breakpoint in both directions while scrolled, checking that the visible packet identity survives the change between document and internal scrolling. The desktop case also filters a scrolled list down to one row, asserts its scroll reset and unchanged page position, then restores the feed. No virtualizer source change was needed. cd frontend && PLAYWRIGHT_PORT_BASE=4363 npx playwright test feed-virtualizer.spec.ts --project=public-desktop --workers=2 --trace=retain-on-failure --output=../.ukmesh-tools/stretch-feed-results: 2/2 pass, no retries or page errors.

Owner-cache verification now checks a real warm refresh: inclusive latest-bucket updates replace the old aggregate, earlier history remains, and the rolling seven-day cutoff removes expired points. A failed warm refresh keeps useful foreground data and permits retry; TTL expiry starts a fresh bounded-window query rather than reusing an old cursor. These use fixed mocked dates and require no source change. cd backend && node --import tsx --test src/owner/ownerLastHopPrewarm.test.ts src/owner/ownerService.test.ts: 14/14 pass.

The expanded cleanup fixture found a second privacy interaction: the private_node_prefixes foreign key's ON DELETE CASCADE fires its statement trigger even when a deleted public node has no prefixes. A separate minimal PostgreSQL experiment records trigger depth 1. Migration 042's depth guard therefore treats this as direct prefix maintenance, advances the public generation and leaves the packet materialization fence stale. The isolated current-trigger fixture reproduced generation 2 -> 3, fence remaining 2. PostgreSQL documents that foreign-key referential actions execute ordinary SQL and fire referencing-table triggers.

Both cleanup policies now lock the public visibility state before archival and deletion. Since selected identities have no privacy marker/prefix and their node rows remain locked, this removal cannot change packet privacy bits. Only an already-current materialization fence is carried forward to the new public generation, in the same transaction. An existing mismatch or missing materialization is never certified. Visibility-lock failure admits no archive or delete; fence-update failure rolls back both. No migration or trigger definition is changed.

The fixture installs the exact relevant functions from migrations 042/049/051, with production node/prefix trigger names, prefix FK, and packet-path classifier. It checks both cleanup policies, retained private packets and packet paths, preserved mismatched-prefix states, current fences after public deletion, and real rollback on archive/delete/fence-update errors. Archive replay into the disposable fixture restores complete node and both visibility records, including nullable fields and a populated additional hardware column. The optional SQL command above now passes 10/10, with no skips; the focused cleanup unit command passes 9/9. Full backend npm test: 356/356, no failures or skips. Backend typecheck/build, frontend build and the 63 API + 11 operator contract check pass. Concurrent production ingestion and visibility-lock duration remain unmeasured; this is stronger isolated verification, not a live database claim.

A controlled-arrival SQL fixture then reproduced another inactivity gap: fresh source-node RF/observer sightings inserted between the initial candidate snapshot and the visibility lock were still deleted (4 nodes removed instead of 2). packetBatch.ts takes FOR KEY SHARE on the same visibility row before updating those rollups. Inactive cleanup now takes its FOR UPDATE lock and rechecks the original candidates against the complete freshness/privacy predicate before archival; the row lock fences later packet-ingestion batches until commit. Candidates withdrawn by new sightings produce neither removal archives nor deletes. An all-withdrawn pass returns the usual empty result. The old MQTT observer policy retains its own predicate.

The controlled arrival passes against actual SQL after the fix, and the full optional fixture suite passes 11/11, with no skips. This models the query boundary in one disposable connection; it is not a concurrent-ingestion load test. cd backend && node --import tsx --test src/maintenance/staleMqttObservers.test.ts: 10/10. Backend typecheck passes; cd backend && npm test: 357/357, no failures or skips.

The owner cache responsiveness regression failed before the fix: a fresh foreground cache hit joined a pending forced prewarm query and did not resolve until that query finished. Fresh foreground reads now return the existing cache immediately. Cold/expired reads and forced refreshes still share in-flight work. The test holds a background refresh open, checks the immediate cached response, checks overlapping warm requests issue one query, and then checks the refreshed result. Focused owner/prewarm tests pass 15/15; backend typecheck and full npm test pass 358/358, with no failures or skips.

At this safety checkpoint all implementation changes were committed and pushed on the private branch. The expanded browser run on isolated ports 4363–4365 finished 54/55: the multi-viewport mobile map test timed out at page.goto with waitUntil: 'networkidle'. Its retained trace and test-server output were investigated and resolved in the following sections; the later complete 61-case run passed. The two feed cases passed at this checkpoint, including the new breakpoint and filter checks.

The viewport trace showed the first two of seven sequential map navigations consuming about 15 seconds of one 30-second test budget, including public OpenFreeMap requests. The viewport regression is now seven independently timed cases; each still loads the map from scratch, checks all geometry, and verifies UK-only API requests. No timeout or geometry tolerance was increased. cd frontend && PLAYWRIGHT_PORT_BASE=4363 npx playwright test test/e2e/mobile.spec.ts --grep 'live map stays inside' --workers=2 --trace=retain-on-failure --output=../.ukmesh-tools/stretch-viewport-results: 7/7 pass, no retries. The complete matrix now contains 61 cases and will be rerun.

Strengthening the terrain check reproduced a test-harness gap: local DEM requests returned 200 text/html, and the old RF PNG also produced image decode errors. Shared test routes now serve a valid 256px RGB flat Terrarium tile (zero metres) and a valid 256px RGBA RF tile. OpenFreeMap style/metadata responses are local; the metadata race still delays the response by 1.5s. The two RF metadata stages now stop advancing their revision after completion. Terrain checks require successful PNG responses, decode both images in the browser, check dimensions and reject terrain/RF tile-load error messages. Real DEM rendering takes more time than rejecting HTML; that test has a 60s budget, with existing UI assertion timeouts unchanged.

cd frontend && PLAYWRIGHT_PORT_BASE=4363 npx playwright test test/e2e/dashboard.spec.ts --grep 'RF coverage remains available with 3D terrain|metadata winning' --workers=1 --trace=retain-on-failure --output=../.ukmesh-tools/stretch-terrain-serial-results: 4/4 pass, no retries. A prior parallel run of the stricter checks had startup timeouts and a dynamic import failure with ERR_NETWORK_CHANGED; its traces retain that evidence. The final matrix uses one browser worker to reduce local software-rendering contention. These synthetic tiles verify loading/decoding behavior, not real terrain elevation accuracy or production rendering performance.

A metrics-scrape regression also reproduced a synchronous Redis client-factory failure escaping before the bounded heartbeat read: the whole registry scrape threw instead of replacing a previous healthy link value. The collector now catches initialization failure and exports -1, while keeping gauge writes outside that catch. The next scrape can initialize successfully and recover. cd backend && node --import tsx --test src/health/workerHeartbeat.test.ts: 6/6 pass; backend typecheck passes. The first full backend rerun was 358/359 because the unchanged worker-pool replacement test timed out its fresh worker startup inside a 750ms budget under concurrent verification load. The worker-pool correction and passing full rerun are described below.

The worker-pool regression now controls the 750ms deadline with Node's mock clock rather than imposing that same wall-time budget on replacement thread startup. It checks that active and queued jobs remain pending at 749ms, both expire at 750ms, queues drain, and a real replacement worker returns a new job. Its independent test-runner deadline remains 10 seconds. cd backend && node --import tsx --test src/path-beta/workerPool.test.ts: 3/3 pass. The full backend rerun then passed 359/359, no failures or skips. No production worker-pool timeout or scheduling code changed.

The first complete matrix with real raster fixtures finished 59/61. The remaining traces showed the owner first-load assertion checking while the page still displayed Loading..., and the desktop map's post-reload assertion looking for Diagnose before the second bootstrap had finished. Both checks had only the default 5-second assertion budget. Owner tests now use the shared map metadata fixture and a valid runtime-config response; first-load readiness gets 15 seconds. The map-mode test explicitly awaits Live Map after reload using the same 15-second readiness check as its initial load, before testing persistence. Polling/lifecycle and mode-state assertions keep their normal budgets. cd frontend && PLAYWRIGHT_PORT_BASE=4363 npx playwright test test/e2e/owner.spec.ts test/e2e/dashboard.spec.ts --grep 'session polling|owner map construction|map modes' --workers=1 --trace=retain-on-failure --output=../.ukmesh-tools/stretch-bootstrap-results: 4/4 pass, no retries. Backend build also passes after the collector fix. The subsequent complete matrix passed 61/61, as recorded below.

Feed geometry now also checks 1,413 fractional viewport/overscan windows across 50 mixed-height rows, including positions before/after the list and overscan larger than the entire list. A direct row-intersection oracle checks every visible row is retained; rendered rows plus top/bottom spacers must equal the complete measured height. cd frontend && node --import tsx --test src/hooks/measuredVirtualRows.test.ts: 3/3 pass. Full frontend npm test: 100/100, no failures or skips. No feed runtime code changed in this step.

The complete browser rerun with explicit readiness and valid local raster fixtures passed 61/61, no retries or skips, in 5.6 minutes: cd frontend && PLAYWRIGHT_PORT_BASE=4363 npx playwright test --workers=1 --trace=retain-on-failure --output=../.ukmesh-tools/stretch-matrix-ready-results. Frontend TypeScript/Vite build also passes after the new geometry checks, with the existing chunk-size warning. All code/test changes are committed and pushed; the working tree is clean at this checkpoint. The burn remains active until the requested stop signal.

Further lock-order verification reproduced 40P01 deadlocks for both cleanup policies on a disposable native PostgreSQL 16.14 cluster: cleanup locked nodes before waiting for visibility, while a separate ingestion transaction held visibility FOR KEY SHARE and then updated the same node. A third case reproduced a busy-node lock timeout (55P03). The cluster uses a fresh ignored data directory, an allocated loopback port, synthetic credentials and automatic shutdown/removal. No existing database or container is used. The standalone test tool is embedded-postgres 16.14.0-beta.17, installed only under ignored tooling.

Both policies now discover candidates without node locks, lock visibility, then re-evaluate the complete original predicate and lock eligible nodes with FOR UPDATE SKIP LOCKED. This matches ingestion's lock order and avoids waiting on busy row writers that may themselves need visibility. Fresh/private changes withdraw their candidates; busy nodes remain for a later pass. Archival and deletion still happen only after successful revalidation and node locking.

Native lock tests changed from 0/3 to 3/3 pass, no skips:

cd backend
TEST_NODE_CLEANUP_POSTGRES_MODULE="file://$PWD/../.ukmesh-tools/embedded-postgres/node_modules/embedded-postgres/dist/index.js" node --import tsx --test src/maintenance/staleMqttObservers.native.integration.test.ts

The native fixtures execute the actual cleanup function with simultaneous SQL connections and the visibility/node locking pattern from packetBatch.ts. They do not execute the entire ingestion pipeline or measure live workloads. The PGlite boundary hook now identifies the unlocked discovery query; its RF freshness assertions remain intact. Two added boundary cases use the actual privacy triggers to retain new consent and private packet flags for both policies. Those consent cases remain controlled single-connection fixtures. The optional PGlite suite passes 13/13; focused cleanup units pass 11/11; full backend npm test passes 360/360, all without failures or skips. Backend typecheck passes. Production lock duration/candidate volume and other concurrent writers remain unmeasured.

Two additional native cases keep the node timestamps stale while a competing visibility-pinned transaction commits only a source-network or observer-region RF sighting. Inactive cleanup observes each new sighting after acquiring visibility and withdraws the node without an archive or delete. The native suite now passes 5/5, with no skips; the same command above applies. Backend typecheck passes, and all temporary native database directories are removed after the run.

Four more native cases commit a new privacy prefix or private-name marker while each cleanup policy waits for an exclusive visibility lock. Both policies observe the committed consent, retain the node and leave the archive empty. The native suite passes 9/9, no skips; backend typecheck passes and no temporary native database directories remain. These cases model the consent writer's visibility-lock protocol with minimal tables; the separate SQL fixture suite executes the full production privacy trigger chain.

Two reverse-order cases execute the production lock_packet_visibility_for_node_privacy_change BEFORE function. Cleanup first locks visibility; a competing private-name UPDATE locks the node and then waits inside that trigger. Both cleanup policies skip the busy row, commit and release visibility, allowing consent to commit without a deadlock or archive. A later pass retains the now-private node. The native suite passes 11/11, 0 skipped; backend typecheck passes and every fixture directory is removed. Only this BEFORE function is installed in those cases; the remaining production privacy materialization chain is covered by the separate SQL fixture suite.

Three further native PostgreSQL 16.14 cases now execute that entire relevant production privacy trigger chain. They confirm that the empty public-node prefix FK cascade advances visibility generation, that an already-current packet fence advances with it, and that pre-existing gapped or missing fences remain uncertified. Stored public packet/path flags remain unchanged, and the node is archived before deletion. The native suite passes 14/14, no skips; the PostgreSQL-WASM suite remains 13/13, no skips. Both engines now use cleanupPrivacyFixture.ts, moved without changing its function/trigger implementation. Backend typecheck passes and no native fixture directories remain. These are isolated schemas/functions, without production migrations, TimescaleDB or live data.

Last-hop regrouping during warm refreshes

Two new regressions reproduced stale groups after successful incremental refreshes. The SQL groups by bucket, peer identity, current name and resolution; the cache previously upserted those keys without removing groups that the new query no longer returned. A peer rename or unresolved-to-resolved transition doubled the affected bucket's sample count, and an empty query retained obsolete groups. The tests failed 2/2 before the fix.

The cache now replaces all groups at or after the inclusive query cursor while retaining earlier buckets and the rolling seven-day trim. Query scope, cache keys, single-flight and failure preservation are unchanged. This also preserves the repository's current ordering within each refreshed bucket. Exact focused command: cd backend && node --import tsx --test src/owner/ownerService.test.ts src/owner/ownerLastHopPrewarm.test.ts: 17/17 pass, 0 failures or skips. Backend typecheck passes. This verifies response replacement with controlled repository results; no live owner query is claimed.

An optional isolated SQL fixture now executes the actual createOwnerRepository.fetchLastHopStrength query through the service. It seeds three current-bucket packets, renames a peer, adds a newly resolvable peer and checks that the refreshed result still has exactly three current samples with the correct averages. Removing those current packets leaves only the earlier cached history. 1/1 passes, 0 skipped, with:

cd backend
TEST_OWNER_LAST_HOP_PGLITE_MODULE="file://$PWD/../.ukmesh-tools/pglite/node_modules/@electric-sql/pglite/dist/index.js" node --import tsx --test src/owner/ownerService.integration.test.ts

The fixture uses PostgreSQL-WASM with minimal identity tables, an identity canonicalizer and a one-hour time_bucket shim. It does not exercise the real identity views, alias reconciliation, TimescaleDB extension or production load. Backend typecheck passes with the fixture included.

The cleanup freshness audit traced last_path_evidence_at to trustworthy multibyte flood-path evidence carrying the packet's observation time. The legacy last_predicted_online_at writer records resolution time; read-only path evaluation explicitly disables that update. Cleanup conservatively retains both signals, and the SQL fixtures verify that fresh values prevent deletion. Production resolution frequency and resulting retention are unmeasured.

Feed verification in Firefox and WebKit

frontend/playwright.feed-cross-browser.config.ts adds an optional profile for the two existing responsive feed cases in Firefox 153.0 and Playwright WebKit 26.5 on Linux. The default profile still lists 61 Chromium cases; the optional profile lists 4, runs one worker and has zero retries.

The first cross-browser run passed 1/4: immediate anchor reads could run before React committed rows after a programmatic scroll. Anchor capture now waits on the normal assertion deadline for a row intersecting the viewport, and preservation checks use a nullable read within their existing poll. The one-pixel position/contiguity limits and virtualization bounds remain intact. The next run passed 3/4; WebKit kept the search caret visible during the desktop-to-mobile resize. Completing the search interaction with blur before resuming browsing makes that case pass using the existing virtualizer hook. The complete optional profile now passes 4/4, no skips or retries. The affected Chromium feed cases also pass 2/2.

Exact optional-profile command used on this host:

cd frontend
LD_LIBRARY_PATH="$PWD/../.ukmesh-tools/webkit-system-libs/root/usr/lib/x86_64-linux-gnu" PLAYWRIGHT_BROWSERS_PATH="$PWD/../.ukmesh-tools/playwright-browsers" PLAYWRIGHT_PORT_BASE=4413 npx playwright test --config=playwright.feed-cross-browser.config.ts --trace=retain-on-failure --output=../.ukmesh-tools/feed-cross-browser-final-results

The browsers were downloaded with PLAYWRIGHT_BROWSERS_PATH pointing into ignored tooling. This host required libevent-2.1-7t64, libgstreamer-plugins-bad1.0-0, libavif16, libgav1-1 and libyuv0; apt download and dpkg-deb --extract placed them under ignored tooling, and their libraries were linked into the ignored WebKit bundle's sys/lib. Its launcher replaces LD_LIBRARY_PATH, so the bundle links are necessary here. No system package install or environment-file change was made. These Linux engine tests do not establish behavior on a physical Safari/iOS device.

Additional exact commands:

cd frontend
PLAYWRIGHT_PORT_BASE=4433 npx playwright test feed-virtualizer.spec.ts --project=public-desktop --workers=1 --trace=retain-on-failure --output=../.ukmesh-tools/feed-chromium-final-results
npx tsc --ignoreConfig --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 --skipLibCheck --types node --strict playwright.feed-cross-browser.config.ts test/e2e/feed-virtualizer.spec.ts
npm run build

Chromium: 2/2 pass; direct config/test typecheck passes; production TypeScript and Vite build pass with the existing chunk-size warning. TypeScript requires --ignoreConfig for this ad hoc file-based check (the first invocation without it reported TS5112). The frontend unit suite remains 100/100.

Final verification and publication

Source commits: 9a5e838 (retirement), 2d2b425 (prewarm), 236e948 (heartbeat), 1f2bc76 (feed), 4b30eb0 (Compose guard), 6dda582 (cleanup). Continuation commits: ab5d5b8 (cleanup privacy), 56bfcb6 (Compose pipefail), 2f8f03e (scheduler regressions), bbb14a1 (E2E isolation), fb981e0 (feed filter/arrival regressions), 62d9a9f (map-mode cold-start budget). Each change stays on the private branch. Further verification commits: 4a73ca0 (reporter-matched alerts), 2d6b703 (feed breakpoint/filter checks), 29c5111 (incremental owner-cache checks), a030ea7 (cleanup materialization fence), 9c7cb49 (late-sighting recheck), e754f46 (responsive fresh-cache reads), 2960f5f (separate viewport cases), f5ac2c9 (valid raster fixtures), a705b89 (Redis initialization recovery), b561632 (deterministic worker deadlines), 6cf1f70 (bootstrap readiness), 3689c01 (fractional feed-range invariants), 77b4ae7 (cleanup lock order), 62597ee (concurrent RF arrivals), c09fcc4 (last-hop group replacement), d67ac63 (actual last-hop SQL), 17a41aa (committed consent). The final documentation commit records this audit and the validation below. Full unit suites are rerun after it, before publication.

Checkpoint ace67e4 passed the complete local Chromium matrix 61/61 in 4.7 minutes, with no retries or skips, after the new anchor-readiness/focus fixture changes. Post-commit backend/frontend unit suites passed 362/362 and 100/100, respectively. The optional Firefox/WebKit profile passed 4/4, and the affected Chromium feed cases passed 2/2. After the shared privacy-trigger fixture and native fence cases were committed, fcac2a9 also passed both full unit suites (362/362 backend, 100/100 frontend, no failures or skips) and the backend build.

Exact command Result
cd backend && npm test 362/362 pass, 0 fail, 0 skipped. Expands to the brief's node --import tsx --test $(find src -name '*.test.ts' ! -name '*.integration.test.ts' -print).
cd frontend && npm test 100/100 pass, 0 fail, 0 skipped.
cd backend && npm run typecheck Pass.
cd backend && npm run build Pass.
cd frontend && npm run build TypeScript + Vite pass; existing chunk-size warning.
cd backend && npm run contract:check Pass: 63 API + 11 operator routes current.
cd frontend && PLAYWRIGHT_PORT_BASE=4273 npx playwright test feed-virtualizer.spec.ts --project=public-desktop 2/2 pass.
cd frontend && PLAYWRIGHT_PORT_BASE=4273 npx playwright test --workers=2 Historical 55/55 pass; the expanded run was 54/55 before the viewport case split and local map fixtures. The later 61-case matrix passes.
cd frontend && PLAYWRIGHT_PORT_BASE=4363 npx playwright test --workers=1 --trace=retain-on-failure --output=../.ukmesh-tools/stretch-matrix-final-results Historical 59/61 before owner/map-reload readiness checks; affected cases and the subsequent complete matrix pass.
cd frontend && PLAYWRIGHT_PORT_BASE=4363 npx playwright test --workers=1 --trace=retain-on-failure --output=../.ukmesh-tools/stretch-matrix-ready-results 61/61 pass, all four projects, no retries or skips.
cd frontend && PLAYWRIGHT_PORT_BASE=4363 npx playwright test --workers=1 --trace=retain-on-failure --output=../.ukmesh-tools/matrix-ace67e4-results 61/61 pass on ace67e4, all four projects, no retries or skips, 4.7 minutes.
Final Chromium feed command above 2/2 pass after anchor-readiness/focus fixture changes.
Optional Firefox/WebKit profile command above 4/4 pass, no retries or skips, including both responsive widths in each engine.
Direct cross-browser config/test typecheck above Pass with --ignoreConfig; first invocation reported TS5112.
Fresh-cache targeted browser command below 4/4 pass with fresh caches; no retries.
.ukmesh-tools/promtool check rules logging/rules/meshcore.yml 24 rules valid.
.ukmesh-tools/promtool test rules logging/rules/meshcore.test.yml 10 scenario groups pass.
PYTHONPATH=viewshed-worker python3 -m unittest discover -s viewshed-worker/tests -p test_link_worker_heartbeat.py -v 5/5 publisher lifecycle tests pass; recording Redis clients, actual heartbeat thread, no live service.
bash -n scripts/check-compose-adoption.sh scripts/replace-container.sh scripts/test-replace-container.sh Pass.
TMPDIR="$PWD/.ukmesh-tools/tmp" bash scripts/test-replace-container.sh 16/16 mocked drills pass.
Optional isolated cleanup PGlite integration command above 13/13 pass, 0 skipped.
Optional isolated cleanup native PostgreSQL command above 14/14 pass, 0 skipped: 11 concurrent-writer cases plus 3 fence cases using the full relevant production trigger chain.
Optional isolated last-hop PGlite integration command above 1/1 pass, 0 skipped; actual repository SQL, peer regrouping, averages and empty refresh.
git diff --check 73ee004 Pass; scoped review found no migration, schema, environment-file, lockfile or unrelated source edits.
cd frontend && npm run lint:css Existing 6 duplicate-selector failures in unchanged files; left out of scope.

Fresh-cache command, from the worktree root:

cd frontend
PLAYWRIGHT_PORT_BASE=4303 npx playwright test feed-virtualizer.spec.ts public.spec.ts dashboard.spec.ts --project=public-desktop --project=dashboard-mobile --grep 'measured feed rows|tablet topology|map modes' --workers=2 --trace=retain-on-failure --output=../.ukmesh-tools/cold-final-results

Completion audit against the supplied brief:

Requirement Reviewable result and evidence
Retired-workload unit-suite repair Historical policy and current callers restored; 3 focused tests and the complete backend suite pass.
Owner prewarm observability and bounded load Progress every ten nodes, in-flight 20s warning, configurable cap of two, ownership-scoped single-flight, responsive fresh-cache reads, replacement of renamed/reclassified groups and shutdown; 17 focused tests pass. Live cold-pass time/load remains unmeasured.
Link heartbeat source and alert semantics Per-scrape genuine Redis heartbeat, including client-initialization failure/recovery; 6 source/metric tests, 5 publisher lifecycle tests and 10 Prometheus scenario groups, including independent scrape reporters. Live alert firing is not claimed.
Variable-height UK feed ResizeObserver measurements and anchored offsets at both widths; 3 frontend geometry tests, 2 Chromium feed cases and 4 Firefox/WebKit cases pass.
Compose adoption guard Local failing mocked reproduction, pre-mutation rejection, 16 passing replacement drills, including the pipefail regression. No live Docker inspection.
Inactive-node archive/delete for every role Role/bridge-independent selection, archive-before-delete transaction, private identity retention, late-sighting revalidation, visibility-before-node lock order and preservation of a current privacy fence; 11 unit, 13 PostgreSQL-WASM and 14 native PostgreSQL fixtures pass. No live deletion or migration.
Final verification and private-branch publication Full backend/frontend units run on the final documentation HEAD; non-forced branch push and matching remote SHA required before handoff.
Other heartbeat proxies Source/cadence/alert-consumer findings recorded above; the health-worker false-positive candidate remains a source-based follow-up.

Publication command: git push -u origin fix/ukmesh-burn-20261002. Verify with git rev-parse HEAD and git ls-remote --heads origin fix/ukmesh-burn-20261002; the two full SHAs must match. Remote ukmesh-w5 remains 73ee00406e7cb0b7251220abbff73144615a8b90. No force push is used. The operator reviews against that integration base; opening a draft PR is optional. Inspected workflow triggers: branch pushes and pull requests request CI. An earlier gh run list --branch listing showed no runs; the explicit repository Actions API subsequently returned the branch's actual runs. On ace67e4, Frontend mobile passed; the pull-request CI backend, frontend and secret-scan jobs passed. The Workers and Compose job and push CI were still running at the 2026-10-02 18:46 UTC check; a complete CI verdict is not claimed for that checkpoint. These jobs use disposable GitHub runner stacks. All three repository workflows are active. release.yml requires a published release or explicit dispatch. No deployment/release workflow is invoked. Current results are linked from the PR Checks tab. Ignored local logs contain TAP/build/browser output, and ignored tooling holds standalone promtool, PGlite and embedded-postgres. None is part of the shipped runtime. Draft PR: https://github.com/gadgethd/ukmesh/pull/113, against ukmesh-w5.

Stop-signal handoff — 2026-10-02 19:06 UTC

Ben's wrap-up signal ends the burn. The latest implementation/test checkpoint is 7a1598e660367879a91762f55bb45e8474f53034, already pushed with a matching remote SHA and a clean worktree. Both full unit suites passed on that commit: 362/362 backend, 100/100 frontend, zero failures or skips. The five Python heartbeat-publisher tests pass. The final commit only records this handoff; both full unit suites are rerun on it before the final summary, where its exact SHA and remote verification are reported.

The four priority items and both implementation stretch items are complete on the private branch. Draft PR #113 remains open against ukmesh-w5. At the 19:06 UTC check, 7a1598e's frontend, secret-scan and mobile checks had passed; backend and Workers/Compose CI were still running. This is not a complete green remote-CI verdict. The final documentation push may supersede these runs; use the PR Checks tab for their current state.

Remaining work is operator review/merge/deployment and production validation: DB load and lock duration, cleanup candidates, cold prewarm duration, actual Redis freshness and alert recovery, and Compose adoption on the live host. The health-worker snapshot-age/cadence mismatch remains the source-based follow-up documented above. Linux WebKit testing does not verify a physical Safari/iOS device. Existing CSS lint failures and the chunk-size warning remain. The requested push.md was unavailable, so its document-specific discipline cannot be verified; pushes were non-forced and remote SHAs were checked.

Optional deeper coverage was not added after the stop signal: native PostgreSQL archive/delete/fence failure injection for both cleanup policies, and native current/gapped/missing-fence cases for the older MQTT-only policy. The existing PostgreSQL-WASM suite covers the three failure stages and both policies' privacy behavior. Native PostgreSQL coverage remains 14 cases: 11 concurrent-writer cases and three full-trigger fence cases for inactive-node cleanup. No incomplete edits or new production scope remain in the worktree.

Deployment gate, limitations and open questions

The operator must separately review, merge and deploy code/rules. Local tests cannot prove live DB load, cold-pass wall time, Redis freshness in production, alert recovery or container adoption. This session will not perform those operations. No migrations were run, no environment files were changed, and no live checkout or other burn worktree was accessed. push.md remains unavailable; the exact requested document-specific push discipline is not claimed.


Backlog round 2 — ukmesh (2026-10-03)

Worktree: /home/ben/worktrees/meshcore-analytics/backlog2-20261003 Branch: backlog2-20261003 Base: b40793f (origin/main, PR #114 re-land)

This section records the five items assigned in BRIEF-R2-B. The pre-existing review above is preserved; it belongs to an earlier burn and branch.

#455 — dead-only vacuum backlog and alerting

Status: fixed in the analytics repository.

  • backend/src/health/status.ts now counts a table when either the existing relative dead-row condition matches or n_dead_tup > 50000, so a deleted TimescaleDB chunk is not hidden by a small n_live_tup estimate.
  • logging/rules/meshcore.yml adds DatabaseVacuumBacklog; its test fixture proves that a table with one live row and 60,000 dead rows fires after 10m.
  • docs/operations.md documents the bounded operator response and explicitly rules out deleting chunks or the database volume.

Proof: vacuum health counts dead-only tables and preserves severity thresholds passes; pinned promtool check config and promtool test rules both pass. The rule and health gate are repository changes only—deployment and live alert verification remain for the normal operator rollout.

#447 — cleanup of role 1/3 and never-bridged nodes

Status: already fixed on the supplied base; no duplicate patch was added.

b40793f already contains the role/bridge-independent cleanupInactiveNodes predicate. backend/src/maintenance/staleMqttObservers.test.ts asserts that inactive cleanup has no role or prior-MQTT requirement, and the optional PostgreSQL fixture PostgreSQL archives inactive roles 1/3 and never-bridged nodes while retaining every fresh clock covers the reported cases. This verifies the root-cause fix in the branch without claiming that the historical 5,287 live rows were deleted during this repo-only task.

#426 — corrupted advert-key variants

Status: fixed for new ingest in the analytics repository.

backend/src/mqtt/decodePacket.ts verifies the complete MeshCore Ed25519 advert signature before identity is trusted. backend/src/mqtt/client.ts records the raw packet telemetry but does not expose the advertised identity or call evaluateAdvert/upsertNode for an invalid signature. Existing bogus rows are not mass-deleted here: that would require a reviewed data-remediation operation outside this no-deploy worktree task.

Proof: only a valid Ed25519 advert may establish node identity passes with a known valid advert and a tampered advert; the advert-count integrity test also guards that upsert remains inside the verification gate. TypeScript and the full backend suite pass.

#436 and #435 — Watchtower /watch

Status: omitted from this repository by an explicit source boundary.

The assigned Watchtower code is in the separate gadgethd/meshcore-discord-bot repository (discord.ts and the monitor JSON identified by the sweep report), which is not present in this worktree. No analytics-side integration point can change its ambiguous-match wording/candidate suggestions (#436) or refresh monitor display names after node renames (#435). No sibling checkout was read or modified, so these require a separate PR in the Watchtower repository.

Validation and constraints

  • Focused backend tests: 20 passing, including #455, #426, and the existing #447 cleanup assertions.
  • npm run typecheck: passed.
  • npm test: 408 passing plus the synthetic alert-receiver check.
  • Prometheus config and rule tests: passed with the CI-pinned promtool image.
  • No VPS deployment, database cleanup, container restart, merge, or unrelated branch push was performed.

PR link: https://github.com/gadgethd/ukmesh/pull/117