Repository navigation
fix(nodes): keep active observers as nodes and explain inactive ones (#199) - #203
Conversation
…e not found" dead-end (#199) Red on master: - ingestor: MoveStaleNodes retires a node whose pubkey is an observer seen within nodeDays (case differs: observer ids are upper-case). - server: a 404 from /api/nodes/{pubkey} carries nothing about the key's inactive_nodes row or observer row. - frontend: api() drops the error body; nodes.js has no explanation for an inactive or observer-only device. - E2E (seed-199 on the fixture): observer detail -> "View node detail" ends on a bare "Node not found". Guards that already pass: a quiet observer and a plain stale node are still retired, a NULL observer id does not block retention, and blacklisted/hidden identities keep the bare 404. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…199) A. Ingestor: MoveStaleNodes no longer retires a node whose pubkey is an observer seen within nodeDays. One predicate drives both the copy to inactive_nodes and the delete; ids are compared via lower() (observer ids are upper-case), NULL ids are skipped so NOT IN cannot block retention, and last_seen stays the last advert. A quiet observer is retired as before. B. Node page: a miss on /api/nodes/{pubkey} answers 404 with nodeNotFoundResponse {error, inactive_node?, observer?} -- two read-only lookups on the 404 path only; blacklisted or hidden identities (identityHidden) and failed lookups keep the bare 404. api() keeps a JSON error body as err.body, and nodes.js renders "No advert heard since <date>; this device is inactive" with the inactive row's name and role, or "No node record" for an observer that never advertised, each linking to the observer. Unknown keys keep the Kpa-clawbot#1150 "Node not found". The observer-only unit check now also pins the reason (no advert heard); the headline alone let a wording mutant survive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rapport — CS-pve-agent2 PR#203 #199 — head 2c2544bStatus: Draft; all acceptance criteria met; CI green on every job that ran. Not merged, not marked ready. Evidence tags: [T] verified by a test or a run, [K] verified by reading the code, [A] assumption, not verified. Commits
Acceptance criteria
Tests (local)
Mutants (14 run, all killed in the final state)Part A (ingestor):
Part B (server):
Part B (frontend):
Browser
CI (run 37180838265, head 2c2544b)
Remaining items
|
Review — CS-cloud PR#203 observer-node-retention — head 2c2544bDom: APPROVE with nits Independent, read-only review of head Evidence tags: [T] test, run or CI I checked; [A] my analysis of the code; [K] taken from the author's report and not re-run. Findings
There are no P1 or P2 findings. Verification points1. Part A, ingestor (
2. Part B, server and UI
3. Acceptance criteria of #199
6. Rules
Tests and mutantsAll runs below are on the merged tree (head merged into current
My own mutants. The author's set is [K] and was not re-run.
Browser
Not verified
Generated by Claude Code |
Relates to #199
Problem
MoveStaleNodesretires every node whoselast_seen(last advert) is older thanretention.nodeDays. Observer activity never counts, so a repeater that is online as an observer can vanish as a node. The Observers page still lists it, and its "View node detail" link then dead-ends on a bare "Node not found".Plan
nodeDays.e2e-fixture.db.Nothing becomes configurable.
retention.nodeDaysand the map status filter are untouched.A. Ingestor: active observers are not retired
MoveStaleNodes(cmd/ingestor/db.go) uses a single shared predicate for both theINSERT … inactive_nodesand theDELETE:last_seenis not touched, so the node page still shows the last advert.nodeDays), the node is retired as before.id IS NOT NULLguard is needed because a single NULL id would makeNOT INfalse for every node and stop retention entirely. A test covers this.B. Node page explains instead of dead-ending (option 1)
Why option 1: it covers every route to
#/nodes/<pubkey>, not only the observer link (hop links and shared URLs too). It also covers observers that never had a node row at all; hiding the link (option 2) would need the observer page to probe the node table first. It needs no extra request: the explanation travels in the 404 body the page already gets.handleNodeDetailnow answers 404 with the named structnodeNotFoundResponse(cmd/server/node_not_found.go), shaped{error, inactive_node?, observer?}.inactive_nodeis theinactive_nodesrow: name, role,last_seen(the last advert), andfirst_seen.observeris the observers row (id, name,last_seen).hasInactiveNodesTablefromidentity_visibility.gois reused.{"error":"Not found"}, using the sameidentityHiddenrule as Reach. A failed lookup also falls back to the bare 404.map[string]interface{}is added. OpenAPI documents the 404 body.api()attaches a JSON error body aserr.body; a non-JSON body leaves it unset.nodes.jsbuilds the view withmissingNodeView(pubkey, body), a pure function exposed for tests.Tests
cmd/ingestor/issue199_test.go:last_seenis unchanged;cmd/server/issue199_node_not_found_test.go:inactive_nodestable;test-issue-199-missing-node.js(intest-all.sh):api()err.bodywith JSON and non-JSON bodies, both views, the unknown key, and escaping.test-issue-199-inactive-observer-e2e.jsandtest-fixtures/seed-199-inactive-observer.sql(wired intodeploy.ymlafter the fixture migration): Observers → "View node detail" → explanation, plus the observer-only node page.Out of scope
nodeson their next advert. The node page explains them until then. This PR does not move rows back out ofinactive_nodes.🤖 Generated with Claude Code