Commit Graph

2 Commits

Author SHA1 Message Date
Kpa-clawbot 5fa3b56ccb fix(#662): GetRepeaterRelayInfo also looks up byPathHop by 1-byte prefix (#1086)
## Summary

Partial fix for #662.

`GetRepeaterRelayInfo` was reporting "never observed as relay hop" /
`RelayCount24h=0` for nodes that clearly DO have packets passing through
them — visible on the same node detail page in the "Paths seen through
node" view.

## Root cause

The `byPathHop` index is keyed by **both**:
- full resolved pubkey (populated when neighbor-affinity resolution
succeeds), and
- raw 1-byte hop prefix from the wire (e.g. `"a3"`)

`GetRepeaterRelayInfo` only looked up the full-pubkey key. Many ingested
non-advert packets only carry the raw 1-byte hop — so any repeater whose
path appearances are all raw-hop entries returned 0, even though the
path-listing endpoint (which prefix-matches) renders them.

Example node: an `a3…` repeater on staging has ~dozens of paths through
it in the UI but the relay-info function returns 0.

## Fix

Look up under both keys (full pubkey + 1-byte prefix) and de-dup by tx
ID before counting.

## Trade-off

The 1-byte prefix CAN over-count when multiple nodes share a first byte.
This trades a possible over-count for clearly false zeros. The richer
disambiguation done by the path-listing endpoint (resolved-path SQL
post-filter via `confirmResolvedPathContains`) is out of scope for this
partial fix — adding it here would mean disk I/O inside what is
currently a pure in-memory lookup. Worth a follow-up if over-counting
shows up in practice.

## TDD

- Red commit (`test: failing test for relay-info prefix-hop mismatch`):
adds `TestRepeaterRelayActivity_PrefixHop` that builds a non-advert
packet with `PathJSON: ["a3"]`, indexes it via `addTxToPathHopIndex`,
then asserts `RelayCount24h>=1` for the full pubkey starting with `a3…`.
Fails on the assertion (got 0), not a build error.
- Green commit (`fix: GetRepeaterRelayInfo also looks up byPathHop by
1-byte prefix`): the lookup change. All five
`TestRepeaterRelayActivity_*` tests pass.

## Scope

This is a **partial** fix — addresses the read-side prefix mismatch
only. Issue #662 is a 4-axis epic (also covers ingest indexing
consistency, UI surfacing, and schema). Leaving #662 open.

---------

Co-authored-by: corescope-bot <bot@corescope>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-05-05 02:33:27 -07:00
Kpa-clawbot 45f30fcadc feat(repeater): liveness detection — distinguish actively relaying from advert-only (#662) (#1073)
## Summary

Implements repeater liveness detection per #662 — distinguishes a
repeater that is **actively relaying traffic** from one that is **alive
but idle** (only sending its own adverts).

## Approach

The backend already maintains a `byPathHop` index keyed by lowercase
hop/pubkey for every transmission. Decode-window writes also key it by
**resolved pubkey** for relay hops. We just weren't surfacing it.

`GetRepeaterRelayInfo(pubkey, windowHours)`:
- Reads `byPathHop[pubkey]`.
- Skips packets whose `payload_type == 4` (advert) — a self-advert
proves liveness, not relaying.
- Returns the most recent `FirstSeen` as `lastRelayed`, plus
`relayActive` (within window) and the `windowHours` actually used.

## Three states (per issue)

| State | Indicator | Condition |
|---|---|---|
| 🟢 Relaying | green | `last_relayed` within `relayActiveHours` |
| 🟡 Alive (idle) | yellow | repeater is in the DB but
`relay_active=false` (no recent path-hop appearance, or none ever) |
|  Stale | existing | falls out of the existing `getNodeStatus` logic |

## API

- `GET /api/nodes` — repeater/room rows now include `last_relayed`
(omitted if never observed) and `relay_active`.
- `GET /api/nodes/{pubkey}` — same fields plus `relay_window_hours`.

## Config

New optional field under `healthThresholds`:

```json
"healthThresholds": {
  ...,
  "relayActiveHours": 24
}
```

Default 24h. Documented in `config.example.json`.

## Frontend

Node detail page gains a **Last Relayed** row for repeaters/rooms with
the 🟢/🟡 state badge. Tooltip explains the distinction from "Last Heard".

## TDD

- **Red commit** `4445f91`: `repeater_liveness_test.go` + stub
`GetRepeaterRelayInfo` returning zero. Active and Stale tests fail on
assertion (LastRelayed empty / mismatched). Idle and IgnoresAdverts
already match the desired behavior under the stub. Compiles, runs, fails
on assertions — not on imports.
- **Green commit** `5fcfb57`: Implementation. All four tests pass. Full
`cmd/server` suite green (~22s).

## Performance

`O(N)` over `byPathHop[pubkey]` per call. The index is bounded by store
eviction; a single repeater has at most a few hundred entries on real
data. The `/api/nodes` loop adds one map read + scan per repeater row —
negligible against the existing enrichment work.

## Limitations (per issue body)

1. Observer coverage gaps — if no observer hears a repeater's relay,
it'll show as idle even when actively relaying. This is inherent to
passive observation.
2. Low-traffic networks — a repeater in a quiet area legitimately shows
idle. The 🟡 indicator copy makes that explicit ("alive (idle)").
3. Hash collisions are mitigated by the existing `resolveWithContext`
path before pubkeys land in `byPathHop`.

Fixes #662

---------

Co-authored-by: clawbot <bot@corescope.local>
2026-05-05 01:17:52 -07:00