CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Docs CI / build (push) Waiting to run
Docs Deploy (GitHub Pages) / build (push) Waiting to run
Docs Deploy (GitHub Pages) / deploy (push) Blocked by required conditions
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
- 0: sing-box gen overrides fragment listen/port from the inbound row (empty listen -> "::"), Update/Add recompute the stored fragment; non-loopback E2E against the real core binary. - 1: agent fetches the sing-box core from the Gitea release when missing, self-checks it, and only then latches the switch. - 2: node list shows the sing-box core version (heartbeat field, persisted on the node row), xray version as fallback. - 3: public-page fallback returns 404 to anonymous visitors when the page is disabled. - 4: settings restart hint bar removed, dead i18n keys cleaned. - 5: two PUT calls missing JSON headers fixed (audit: only those two), batch probe-task creation endpoint + UI. - 6: cf-probe retirement card and coverage pipeline removed. - 7: generated vless links no longer carry spx (panel export + sub); parser still accepts spx; golden fixtures updated. - probe batch: dedicated probe settings tab (public-page settings, tasks, per-node meta), retention_days editable, per-node traffic calibration + stat-interface pinning, runtime metrics split into a lightweight fast report (default 3s, 1-60s adjustable). - 8: sing-box core processes start with GOMEMLIMIT=32MiB, GOGC=50. - 9: dead /getTwoFactorEnable docs entries removed (frontend call and contract entry already gone at base; verified by grep).
177 lines
5.9 KiB
Go
177 lines
5.9 KiB
Go
package probe
|
|
|
|
import (
|
|
"database/sql"
|
|
"strconv"
|
|
"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
|
|
// TrafficCalUpGB / TrafficCalDownGB are the operator's ledger
|
|
// corrections in GB (V2.0.1), either sign: added to the counted
|
|
// uplink (tx) / downlink (rx) figures wherever the ledger is
|
|
// read — panel statistics, the public page and traffic alerts.
|
|
TrafficCalUpGB float64
|
|
TrafficCalDownGB float64
|
|
// StatIface pins the one interface whose counters the agent
|
|
// books (V2.0.1); "" keeps the automatic counted-set selection.
|
|
StatIface 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,
|
|
traffic_cal_up_gb, traffic_cal_down_gb, stat_iface`
|
|
|
|
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,
|
|
&m.TrafficCalUpGB, &m.TrafficCalDownGB, &m.StatIface); 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,
|
|
traffic_cal_up_gb=excluded.traffic_cal_up_gb,
|
|
traffic_cal_down_gb=excluded.traffic_cal_down_gb,
|
|
stat_iface=excluded.stat_iface`,
|
|
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,
|
|
m.TrafficCalUpGB, m.TrafficCalDownGB, m.StatIface)
|
|
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
|
|
}
|
|
}
|
|
// The B6 offline mark is keyed per agent in probe_setting; the
|
|
// traffic steps are one shared JSON value the sweep prunes to
|
|
// live agents on its next write, so only the mark is struck here.
|
|
if _, err := tx.Exec(`DELETE FROM probe_setting WHERE key = ?`,
|
|
"notify_down:"+strconv.FormatInt(agentID, 10)); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|