Fixes#1151
## Problem
The side-panel "Heard By" row template in `public/nodes.js` (line 1337)
built its stats suffix with inline ternaries:
```js
${o.packetCount} pkts · ${o.avgSnr != null ? '...' : ''}${o.avgRssi != null ? ' · RSSI ...' : ''}
```
When `avgSnr` and/or `avgRssi` were `null` (very common in prod —
many CJS observers have both null), this produced orphan separators:
- both null → `"110 pkts · "` (trailing dot)
- snr null only → `"55 pkts · · RSSI -50"` (double dot)
## Fix
Build a filtered parts array, then `.join(' · ')`. Only present fields
contribute, so the separator can never appear next to nothing.
```js
const stats = [`${o.packetCount} pkts`];
if (o.avgSnr != null) stats.push('SNR ' + Number(o.avgSnr).toFixed(1) + 'dB');
if (o.avgRssi != null) stats.push('RSSI ' + Number(o.avgRssi).toFixed(0));
// → stats.join(' · ')
```
Full-page table (line 1337's neighbor) was already null-safe (separate
`<td>` cells), so only the side-panel template needed the change.
## TDD
Red commit: `1c02ff9a7889aadd16f87f4e673287f9742d4ad0` — adds
`test-issue-1151-orphan-separators-e2e.js` to the deploy.yml E2E job.
The test stubs `/api/nodes/:pubkey/health` via Playwright `page.route()`
with four observer permutations (both null, snr-only-null,
rssi-only-null,
both set), opens the side panel, and asserts no `.observer-row` stat
suffix matches `· ·`, leading `·`, or trailing `·`.
E2E assertion added: `test-issue-1151-orphan-separators-e2e.js:96`
## Preflight
All hard gates pass — see preflight output in the implementation log.
---------
Co-authored-by: CoreScope Bot <bot@corescope>