mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-24 07:21:01 +00:00
## Summary Adds a sortable **First Seen** column to the Nodes table so users can spot newly observed repeaters in their region (per the reporter's use case). Closes #1166 ## Backend `/api/nodes` already exposes `first_seen` per node via `db.scanNodeRow` (sourced from the existing `nodes.first_seen` column — no schema migration, no recomputation, no extra query cost). The red test pins that contract. ## Frontend (`public/nodes.js`) - New `<th data-sort-key="first_seen" data-sort-default="desc">First Seen</th>` between Last Seen and Adverts. - Cell renders via `renderNodeTimestampHtml(n.first_seen)` — same relative-time + absolute-ISO `title=` tooltip as the Last Seen column. Empty values render as `—`. - `sortNodes` gains a `first_seen` branch with **empty-last** semantics: nodes without a `first_seen` always sort to the bottom regardless of asc/desc direction, so unknowns never clutter the top of the table. - Empty-state `colspan` bumped 7 → 8. ## TDD - **Red commit** `112442f4` — `test-issue-1166-first-seen-column.js` + `cmd/server/first_seen_1166_test.go`. The backend half passes on red (field already returned); 5 frontend assertions fail on assertions (column header missing, sort branch missing, empty-last violated). - **Green commit** `9274b36c` — only `public/nodes.js`. All 6 tests pass. Verified red is real-fail (assertion-shaped) by checking out the red commit's `nodes.js` and re-running the test: 5 failures, all on `assert.strictEqual`, none on parse/import. ## Test results ``` node test-issue-1166-first-seen-column.js → 6 passed, 0 failed node test-frontend-helpers.js → 611 passed, 0 failed go test ./cmd/server/... → ok (45.16s, all pass) ``` ## Files changed - `public/nodes.js` (+14 / −1) - `test-issue-1166-first-seen-column.js` (new) - `cmd/server/first_seen_1166_test.go` (new) ## Scope guardrails - No schema migration. - No new files outside the worktree's three allowed surfaces. - No refactor of other Nodes columns. - Empty cells handled in both render (em-dash) and sort (always last). --------- Co-authored-by: fix-1166-bot <bot@corescope.local>
76 lines
2.1 KiB
Go
76 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// TestFirstSeen_1166_HandleNodesSurface pins issue #1166: the /api/nodes
|
|
// response carries a `first_seen` ISO timestamp per node so the frontend
|
|
// can show a sortable "First Seen" column.
|
|
func TestFirstSeen_1166_HandleNodesSurface(t *testing.T) {
|
|
db := setupCapabilityTestDB(t)
|
|
defer db.conn.Close()
|
|
if _, err := db.conn.Exec(`ALTER TABLE nodes ADD COLUMN foreign_advert INTEGER DEFAULT 0`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pk := "cccc000000000000000000000000000000000000000000000000000000000000"
|
|
first := time.Now().Add(-72 * time.Hour).UTC().Format("2006-01-02T15:04:05.000Z")
|
|
last := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
|
|
if _, err := db.conn.Exec(`INSERT INTO nodes
|
|
(public_key, name, role, lat, lon, last_seen, first_seen, advert_count)
|
|
VALUES (?, 'rpt', 'repeater', 37.5, -122.0, ?, ?, 5)`,
|
|
pk, last, first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
store := NewPacketStore(db, nil)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
srv.store = store
|
|
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
req := httptest.NewRequest("GET", "/api/nodes?limit=10", nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
if rr.Code != 200 {
|
|
t.Fatalf("/api/nodes status: want 200, got %d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Nodes []map[string]interface{} `json:"nodes"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v body=%s", err, rr.Body.String())
|
|
}
|
|
var got map[string]interface{}
|
|
for _, n := range resp.Nodes {
|
|
if k, _ := n["public_key"].(string); k == pk {
|
|
got = n
|
|
break
|
|
}
|
|
}
|
|
if got == nil {
|
|
t.Fatalf("node missing from /api/nodes response")
|
|
}
|
|
fs, hasFS := got["first_seen"]
|
|
if !hasFS {
|
|
t.Fatalf("first_seen absent from /api/nodes response (issue #1166)")
|
|
}
|
|
s, _ := fs.(string)
|
|
if s == "" {
|
|
t.Errorf("first_seen empty, want ISO timestamp, got %v", fs)
|
|
}
|
|
if s != first {
|
|
t.Errorf("first_seen = %q, want %q", s, first)
|
|
}
|
|
}
|