package probe import ( "database/sql" "strings" ) // Meta is one agent's probe display/operations metadata (hub node row's // operator-owned half, ITEM7-SCOPE §5.2): what the public page shows // beyond the machine's own reports — price/expiry/plan figures, the // public switch and ordering, grouping and the public remark, the // notify opt-in and the hand-set address/country pins. Rows are // ensured with hub defaults on the agent's first report; the editing // surface (B3) reads and writes them here. type Meta struct { AgentID int64 Price float64 Currency string BillingCycle string ExpiresAt *string // "2006-01-02" or nil TrafficLimit int64 TrafficMode string TrafficResetDay int64 Public bool Sort int64 GroupName string PublicRemark string Notify bool CountryPin string IPv4Pin string IPv6Pin string } // DefaultMeta returns the metadata of an agent that has no row yet — // the same values the row would carry once ensured (the probe_meta // DDL defaults). func DefaultMeta(agentID int64) Meta { return Meta{ AgentID: agentID, Currency: "USD", BillingCycle: "monthly", TrafficMode: "sum", TrafficResetDay: 1, Public: true, } } const metaColumns = `agent_id, price, currency, billing_cycle, expires_at, traffic_limit, traffic_mode, traffic_reset_day, public, sort, group_name, public_remark, notify, country_pin, ipv4_pin, ipv6_pin` func scanMeta(sc interface{ Scan(...any) error }) (Meta, error) { var m Meta var expires sql.NullString var public, notify int64 if err := sc.Scan(&m.AgentID, &m.Price, &m.Currency, &m.BillingCycle, &expires, &m.TrafficLimit, &m.TrafficMode, &m.TrafficResetDay, &public, &m.Sort, &m.GroupName, &m.PublicRemark, ¬ify, &m.CountryPin, &m.IPv4Pin, &m.IPv6Pin); err != nil { return Meta{}, err } if expires.Valid && strings.TrimSpace(expires.String) != "" { v := expires.String m.ExpiresAt = &v } m.Public = public != 0 m.Notify = notify != 0 return m, nil } // Meta reads one agent's metadata row. ok is false when the agent has // no row yet (it never reported); callers then use DefaultMeta. func (s *Store) Meta(agentID int64) (Meta, bool, error) { row := s.db.QueryRow(`SELECT `+metaColumns+` FROM probe_meta WHERE agent_id = ?`, agentID) m, err := scanMeta(row) if err == sql.ErrNoRows { return Meta{}, false, nil } if err != nil { return Meta{}, false, err } return m, true, nil } // AllMeta reads every metadata row, keyed by agent id. func (s *Store) AllMeta() (map[int64]Meta, error) { rows, err := s.db.Query(`SELECT ` + metaColumns + ` FROM probe_meta`) if err != nil { return nil, err } defer rows.Close() out := make(map[int64]Meta) for rows.Next() { m, err := scanMeta(rows) if err != nil { return nil, err } out[m.AgentID] = m } return out, rows.Err() } // UpsertMeta writes one agent's metadata row (insert or full replace). func (s *Store) UpsertMeta(m Meta) error { var expires any if m.ExpiresAt != nil && strings.TrimSpace(*m.ExpiresAt) != "" { expires = *m.ExpiresAt } _, err := s.db.Exec( `INSERT INTO probe_meta (`+metaColumns+`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(agent_id) DO UPDATE SET price=excluded.price, currency=excluded.currency, billing_cycle=excluded.billing_cycle, expires_at=excluded.expires_at, traffic_limit=excluded.traffic_limit, traffic_mode=excluded.traffic_mode, traffic_reset_day=excluded.traffic_reset_day, public=excluded.public, sort=excluded.sort, group_name=excluded.group_name, public_remark=excluded.public_remark, notify=excluded.notify, country_pin=excluded.country_pin, ipv4_pin=excluded.ipv4_pin, ipv6_pin=excluded.ipv6_pin`, m.AgentID, m.Price, m.Currency, m.BillingCycle, expires, m.TrafficLimit, m.TrafficMode, m.TrafficResetDay, boolToInt(m.Public), m.Sort, m.GroupName, m.PublicRemark, boolToInt(m.Notify), m.CountryPin, m.IPv4Pin, m.IPv6Pin) return err } func boolToInt(b bool) int64 { if b { return 1 } return 0 } // DeleteAgentData removes every probe row of an agent that no longer // exists: metadata, the traffic ledger and both history tiers. The // hub keeps history inside its own node table; here probe.db is a // separate file, so the panel-side agent deletion calls this to keep // the two stores in step (B2 report §6 cleanup point). func (s *Store) DeleteAgentData(agentID int64) error { tx, err := s.db.Begin() if err != nil { return err } defer tx.Rollback() for _, table := range []string{ "probe_meta", "probe_traffic", "probe_metric", "probe_metric_hour", "probe_ping_record", "probe_ping_hour", "probe_ping_node", } { if _, err := tx.Exec(`DELETE FROM `+table+` WHERE agent_id = ?`, agentID); err != nil { return err } } return tx.Commit() }