package probe import ( "strconv" "strings" "time" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/logger" ) // RetentionDays reads the history retention window (days) from the // panel settings (key retention_days, as in hub). Default 7 — the // window the user settled on — clamped to [1, 365] (hub's cap). func RetentionDays() int64 { var row model.Setting err := database.GetDB().Where("key = ?", "retention_days").First(&row).Error if err != nil { return DetailDays } n, perr := strconv.ParseInt(strings.TrimSpace(row.Value), 10, 64) if perr != nil { return DetailDays } if n < 1 { return 1 } if n > 365 { return 365 } return n } // Record files one heartbeat's probe sample into the default store. // prevLastSeenMs is the agent row's last-seen stamp from before this // heartbeat (milliseconds, as stored on agent_nodes). Best-effort: any // failure is logged and swallowed so a heartbeat never fails over // history. func Record(agentID int64, smp Sample, prevLastSeenMs int64) { st := Default() if st == nil { return } if err := st.Record(agentID, smp, time.Now(), prevLastSeenMs/1000); err != nil { logger.Warningf("probe: record agent %d: %v", agentID, err) } } // Maintain runs one housekeeping pass against the default store: // roll_up first, then prune — prune runs even when roll_up fails, as // it keeps minute rows until their hour is folded and skipping it // would leave the database growing (hub housekeeping discipline). func Maintain() { st := Default() if st == nil { return } keep := RetentionDays() if folded, err := st.RollUp(time.Now().Unix(), keep); err != nil { logger.Warningf("probe: roll_up failed: %v", err) } else if folded > 1 { logger.Infof("probe: folded %d hours of history into the hourly tier", folded) } if _, err := st.Prune(keep); err != nil { logger.Warningf("probe: prune failed: %v", err) } } // BookHeld books every node's held, not-yet-booked reading on the // default store; called as the master shuts down. func BookHeld() { if st := Default(); st != nil { st.BookHeld() } } // TrafficSnapshot is the B3 read port: every agent's gated ledger. func TrafficSnapshot() (map[int64]Traffic, error) { st := Default() if st == nil { return map[int64]Traffic{}, nil } return st.AllTraffic() } // MetaSnapshot is the B3 read port for display metadata: every agent's // probe_meta row, keyed by agent id. Agents without a row are absent; // callers substitute DefaultMeta. func MetaSnapshot() (map[int64]Meta, error) { st := Default() if st == nil { return map[int64]Meta{}, nil } return st.AllMeta() } // MetaFor reads one agent's metadata row from the default store. // ok is false when the agent never reported (no row ensured yet). func MetaFor(agentID int64) (Meta, bool, error) { st := Default() if st == nil { return Meta{}, false, nil } return st.Meta(agentID) } // SaveMeta persists one agent's metadata row (B3 edit surface). func SaveMeta(m Meta) error { st := Default() if st == nil { return nil } return st.UpsertMeta(m) } // MetricHistory is the B3 read port for one agent's history window. // With no store the window is simply empty. func MetricHistory(agentID int64, sp Span) ([]MetricRow, error) { st := Default() if st == nil { return []MetricRow{}, nil } return st.MetricHistory(agentID, sp) } // DeleteAgentData drops a removed agent's probe rows (B3 cleanup point // called from agent deletion). Best-effort: failures are logged and // swallowed, as a stale probe row must never block a panel operation. func DeleteAgentData(agentID int64) { st := Default() if st == nil { return } if err := st.DeleteAgentData(agentID); err != nil { logger.Warningf("probe: delete agent %d data: %v", agentID, err) } } // ---- B4: probe tasks, results and the latency read ---- // PingTasks lists every probe task with its assignments. With no // store the list is empty. func PingTasks() ([]PingTask, error) { st := Default() if st == nil { return []PingTask{}, nil } return st.PingTasks() } // SavePingTask creates or replaces one probe task (see Store). func SavePingTask(t PingTask, base *[]int64) (int64, error) { st := Default() if st == nil { return 0, nil } return st.SavePingTask(t, base) } // DeletePingTask removes one probe task and its filed history. func DeletePingTask(id int64) error { st := Default() if st == nil { return nil } return st.DeletePingTask(id) } // SetNodePingTasks applies one agent's assignment difference. func SetNodePingTasks(agentID int64, tasks, base []int64) error { st := Default() if st == nil { return nil } return st.SetNodePingTasks(agentID, tasks, base) } // ReorderPingTasks rewrites the panel order of the task list. func ReorderPingTasks(ids []int64) error { st := Default() if st == nil { return nil } return st.ReorderPingTasks(ids) } // PingTasksFor reads the task list pushed to one agent on its config // poll. Best-effort like the rest of the ingest side: a store failure // reads as "no tasks" and is logged, so a broken probe.db never // breaks config delivery — the agent keeps its current list until a // poll succeeds. func PingTasksFor(agentID int64) []PingTaskRef { st := Default() if st == nil { return []PingTaskRef{} } refs, err := st.PingTasksFor(agentID) if err != nil { logger.Warningf("probe: ping tasks for agent %d: %v", agentID, err) return []PingTaskRef{} } return refs } // PingTaskNames reads one agent's probe chart labels. func PingTaskNames(agentID int64) (map[string]string, error) { st := Default() if st == nil { return map[string]string{}, nil } return st.PingTaskNames(agentID) } // InsertPings files one agent's probe results at the arrival stamp. // Best-effort (heartbeat path): failures are logged and swallowed. func InsertPings(agentID int64, results []PingResult) { if len(results) == 0 { return } st := Default() if st == nil { return } if err := st.InsertPings(agentID, results, time.Now().Unix()); err != nil { logger.Warningf("probe: insert pings agent %d: %v", agentID, err) } } // PingHistory is the B4 read port for one agent's latency window. // With no store the window is simply empty. func PingHistory(agentID int64, sp Span) ([]PingRow, map[string]float64, error) { st := Default() if st == nil { return []PingRow{}, map[string]float64{}, nil } return st.PingHistory(agentID, sp) }