- probe package: probe_ping_task/probe_ping_node tables; task CRUD with hub save semantics (id high-water, base-diff assignment, 64-per-agent crowding refusal, auto_join cap); filing with assignment-gated insert; PingHistory weighted-median chart read over minute+hour tiers; auto-join on first Record (ensureRows). - controller: /panel/api/agent/probe-tasks* management endpoints (node id space, hub validation chain verbatim); series=ping now serves real rows from probe.PingHistory; heartbeat ingests probeResults (missing latency skipped, batch capped); config response carries probe_tasks (always present, empty = clear). - agent: per-task probe loops (immediate first probe, ticker, 900ms resolve/dial budgets, <=3 addresses, resolve overrun = no sample), result buffer drained on heartbeat and requeued on failure; task set applied before the config-hash early return.
154 lines
4.8 KiB
Go
154 lines
4.8 KiB
Go
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()
|
|
}
|