diff --git a/internal/web/controller/agent.go b/internal/web/controller/agent.go index 919651d..ee2d70d 100644 --- a/internal/web/controller/agent.go +++ b/internal/web/controller/agent.go @@ -626,11 +626,16 @@ func probeStatIface(agentID int64) string { } // probeReport ingests one fast report (V2.0.1): a probe metrics -// block and nothing else. It deliberately does NOT touch the -// agent's last-seen stamp — liveness stays the 60 s heartbeat's -// signal, and the traffic ledger's unseen-boot rule keeps measuring -// reboots against heartbeat sightings exactly as before. Agents on -// older masters never see this endpoint (404) and simply keep +// block and nothing else. Since V2.0.3 it also touches the agent's +// last-seen stamp, like every other report channel: the hub's +// themes gate each batch on the node's last_seen being newer than +// the last one they applied, so a metrics-only channel that never +// advanced the stamp froze the public cards between the 10 s +// traffic reports. Liveness now means "any report from the agent" +// — all channels come from the same process — and the traffic +// ledger's unseen-boot rule is unaffected: Record still receives +// the pre-touch stamp, so gaps measure exactly as before. Agents +// on older masters never see this endpoint (404) and simply keep // reporting metrics on the heartbeat; masters accept metrics on // either channel. func (a *AgentController) probeReport(c *gin.Context) { @@ -644,9 +649,13 @@ func (a *AgentController) probeReport(c *gin.Context) { ProbeMetrics *heartbeatProbeMetrics `json:"probeMetrics"` } _ = c.ShouldBindJSON(&req) + // The last-seen stamp as it stood before this report (see the + // heartbeat handler): captured before touchAgent overwrites it. + prevLastSeen := agent.LastSeen + a.touchAgent(uint(agent.Id), "") if req.ProbeMetrics != nil { a.updateAgentProbe(uint(agent.Id), nil, req.ProbeMetrics) - probe.Record(int64(agent.Id), probeSample(req.ProbeMetrics), agent.LastSeen) + probe.Record(int64(agent.Id), probeSample(req.ProbeMetrics), prevLastSeen) } jsonObj(c, gin.H{"message": "ok"}, nil) } diff --git a/internal/web/controller/agent_fast_report_test.go b/internal/web/controller/agent_fast_report_test.go index 10f8689..9ab1c08 100644 --- a/internal/web/controller/agent_fast_report_test.go +++ b/internal/web/controller/agent_fast_report_test.go @@ -18,9 +18,10 @@ import ( ) // V2.0.1 probe batch, master side: the fast-report endpoint ingests -// metrics without touching liveness (last-seen stays the -// heartbeat's), and the config poll carries the cadence + the -// node's counted-interface pin. +// metrics, and the config poll carries the cadence + the node's +// counted-interface pin. Since V2.0.3 the fast report also stamps +// last-seen (the hub's themes gate batches on its freshness); it +// originally did not — see the probeReport handler comment. func TestAgentFastReportAndConfigKeys(t *testing.T) { gin.SetMode(gin.TestMode) dbDir := t.TempDir() @@ -71,6 +72,12 @@ func TestAgentFastReportAndConfigKeys(t *testing.T) { metrics := `{"probeMetrics":{"bootId":"boot-f/0123456789abcdef","uptime":60,` + `"memTotal":1000,"memUsed":400,"netRxTotal":10,"netTxTotal":20}}` + // Age the stamp first so the fast report's touch is observable + // even within the same millisecond as the heartbeat's. + stale := seenAfterHeartbeat - 120_000 + if err := db.Model(&model.AgentNode{}).Where("id = ?", agent.Id).Update("last_seen", stale).Error; err != nil { + t.Fatalf("age last-seen: %v", err) + } if code, body := call(http.MethodPost, "/panel/api/agent/probe-report", metrics); code != http.StatusOK { t.Fatalf("probe-report: %d (%s)", code, body) } @@ -78,8 +85,8 @@ func TestAgentFastReportAndConfigKeys(t *testing.T) { if row.ProbeMemUsed != 400 || row.ProbeAt == 0 { t.Errorf("fast report metrics not stored: %+v", row) } - if row.LastSeen != seenAfterHeartbeat { - t.Errorf("fast report moved last-seen: %d -> %d (liveness is the heartbeat's signal)", seenAfterHeartbeat, row.LastSeen) + if row.LastSeen <= stale { + t.Errorf("fast report did not stamp last-seen: %d -> %d (V2.0.3: themes gate on it)", stale, row.LastSeen) } // Unauthorized callers are refused. diff --git a/internal/web/service/probe_public.go b/internal/web/service/probe_public.go index 6b2cc1e..c361f5a 100644 --- a/internal/web/service/probe_public.go +++ b/internal/web/service/probe_public.go @@ -29,9 +29,11 @@ import ( // 150 s absorbs one lost beat plus jitter. const ProbeOnlineThresholdMs = 150_000 -// ProbeSnapshotTTL mirrors the hub's 1900 ms frame reuse: just under -// the 2 s push interval, so a frame is built at most once per tick. -const ProbeSnapshotTTL = 1900 * time.Millisecond +// ProbeSnapshotTTL mirrors the hub's frame reuse, shortened in +// V2.0.3 from the hub's 1900 ms: just under the fastest (1 s) push +// cadence, so a 1 s stream still gets a fresh frame every tick +// instead of repeating the previous one half the time. +const ProbeSnapshotTTL = 900 * time.Millisecond // ---- public-surface settings (main settings KV, hub key names) ----