fix: update observer last_seen on packet ingestion (#479)

## Summary

Related to #463 (partial fix — addresses packet path, status message
path still needs investigation) — Observers incorrectly showing as
offline despite actively forwarding packets.

## Root Cause

Observer `last_seen` was only updated when status topic messages
(`meshcore/<region>/<observer_id>/status`) were received via
`UpsertObserver`. When packets were ingested from an observer, the
observer's `last_seen` was **not** updated — only the `observer_idx` was
resolved for the observation record.

This meant observers with low traffic that published status messages
less frequently than the 10-minute online threshold would appear offline
on the observers page, even though they were clearly alive and
forwarding packets.

## Changes

**`cmd/ingestor/db.go`:**
- Added `stmtUpdateObserverLastSeen` prepared statement: `UPDATE
observers SET last_seen = ? WHERE rowid = ?`
- In `InsertTransmission`, after resolving `observer_idx`, update the
observer's `last_seen` to the packet timestamp
- This ensures any observer actively forwarding traffic stays marked as
online

**`cmd/ingestor/db_test.go`:**
- Added `TestInsertTransmissionUpdatesObserverLastSeen` — verifies that
inserting a packet from an observer updates its `last_seen` from a
backdated value to the packet timestamp

## Performance

The added `UPDATE` is a single-row update by `rowid` (primary key) —
O(1) with no index overhead. It runs once per packet insertion when an
observer is resolved, which was already doing a `SELECT` by `rowid`
anyway. No measurable impact on ingestion throughput.

## Test Results

All existing tests pass:
- `cmd/ingestor`: 26.6s 
- `cmd/server`: 3.7s 

---------

Co-authored-by: you <you@example.com>
This commit is contained in:
Kpa-clawbot
2026-04-01 23:43:47 -07:00
committed by GitHub
parent 96d0bbe487
commit f9cfad9cd4
2 changed files with 62 additions and 3 deletions
+12 -3
View File
@@ -36,8 +36,9 @@ type Store struct {
stmtUpsertNode *sql.Stmt
stmtIncrementAdvertCount *sql.Stmt
stmtUpsertObserver *sql.Stmt
stmtGetObserverRowid *sql.Stmt
stmtUpdateNodeTelemetry *sql.Stmt
stmtGetObserverRowid *sql.Stmt
stmtUpdateObserverLastSeen *sql.Stmt
stmtUpdateNodeTelemetry *sql.Stmt
}
// OpenStore opens or creates a SQLite DB at the given path, applying the
@@ -369,6 +370,11 @@ func (s *Store) prepareStatements() error {
return err
}
s.stmtUpdateObserverLastSeen, err = s.db.Prepare("UPDATE observers SET last_seen = ? WHERE rowid = ?")
if err != nil {
return err
}
s.stmtUpdateNodeTelemetry, err = s.db.Prepare(`
UPDATE nodes SET
battery_mv = COALESCE(?, battery_mv),
@@ -428,13 +434,16 @@ func (s *Store) InsertTransmission(data *PacketData) (bool, error) {
s.Stats.DuplicateTransmissions.Add(1)
}
// Resolve observer_idx
// Resolve observer_idx and update last_seen
var observerIdx *int64
if data.ObserverID != "" {
var rowid int64
err := s.stmtGetObserverRowid.QueryRow(data.ObserverID).Scan(&rowid)
if err == nil {
observerIdx = &rowid
// Update observer last_seen on every packet to prevent
// low-traffic observers from appearing offline (#463)
_, _ = s.stmtUpdateObserverLastSeen.Exec(now, rowid)
}
}
+50
View File
@@ -516,6 +516,56 @@ func TestInsertTransmissionWithObserver(t *testing.T) {
}
}
// #463: Verify that inserting a packet updates the observer's last_seen,
// so low-traffic observers don't incorrectly appear offline.
func TestInsertTransmissionUpdatesObserverLastSeen(t *testing.T) {
s, err := OpenStore(tempDBPath(t))
if err != nil {
t.Fatal(err)
}
defer s.Close()
// Insert observer with an old last_seen
if err := s.UpsertObserver("obs1", "Observer1", "SJC", nil); err != nil {
t.Fatal(err)
}
// Backdate last_seen to 2 hours ago
oldTime := "2026-03-24T22:00:00Z"
s.db.Exec("UPDATE observers SET last_seen = ? WHERE id = ?", oldTime, "obs1")
// Verify it was backdated
var lastSeenBefore string
s.db.QueryRow("SELECT last_seen FROM observers WHERE id = ?", "obs1").Scan(&lastSeenBefore)
if lastSeenBefore != oldTime {
t.Fatalf("expected last_seen=%s, got %s", oldTime, lastSeenBefore)
}
// Insert a packet from this observer
data := &PacketData{
RawHex: "0A00D69F",
Timestamp: "2026-03-25T01:00:00Z",
ObserverID: "obs1",
Hash: "lastseentest123456",
RouteType: 2,
PayloadType: 2,
PathJSON: "[]",
DecodedJSON: `{"type":"TXT_MSG"}`,
}
if _, err := s.InsertTransmission(data); err != nil {
t.Fatal(err)
}
// Verify last_seen was updated
var lastSeenAfter string
s.db.QueryRow("SELECT last_seen FROM observers WHERE id = ?", "obs1").Scan(&lastSeenAfter)
if lastSeenAfter == oldTime {
t.Error("observer last_seen was NOT updated after packet insertion — low-traffic observers will appear offline")
}
if lastSeenAfter != "2026-03-25T01:00:00Z" {
t.Errorf("expected last_seen=2026-03-25T01:00:00Z, got %s", lastSeenAfter)
}
}
func TestEndToEndIngest(t *testing.T) {
s, err := OpenStore(tempDBPath(t))
if err != nil {