New Features and safe log

This commit is contained in:
2026-05-03 11:05:13 -03:00
parent 43482c88fa
commit c74f6e2282
8 changed files with 823 additions and 26 deletions

117
main.go
View File

@@ -564,6 +564,7 @@ type IfaceTotals struct {
LastKernelRxBytes uint64
LastKernelTxBytes uint64
UpdatedAt time.Time
ResetAt time.Time
}
type IfaceTotalsManager struct {
@@ -582,11 +583,27 @@ func (tm *IfaceTotalsManager) ApplyKernel(iface string, kRx, kTx uint64) (totalR
tm.mu.Lock()
defer tm.mu.Unlock()
now := time.Now()
st, ok := tm.m[iface]
if !ok {
st = &IfaceTotals{Iface: iface}
st = &IfaceTotals{Iface: iface, ResetAt: now}
tm.m[iface] = st
}
if st.ResetAt.IsZero() {
st.ResetAt = now
}
// The live interface counters in the Stats tab are a rolling 30-day total.
// This reset does not touch the vnstat-style daily/monthly history tables.
if now.Sub(st.ResetAt) >= 30*24*time.Hour {
st.TotalRxBytes = 0
st.TotalTxBytes = 0
st.LastKernelRxBytes = kRx
st.LastKernelTxBytes = kTx
st.ResetAt = now
st.UpdatedAt = now
return 0, 0
}
// RX
if st.LastKernelRxBytes == 0 && st.TotalRxBytes == 0 {
@@ -609,10 +626,33 @@ func (tm *IfaceTotalsManager) ApplyKernel(iface string, kRx, kTx uint64) (totalR
}
st.LastKernelTxBytes = kTx
st.UpdatedAt = time.Now()
st.UpdatedAt = now
return st.TotalRxBytes, st.TotalTxBytes
}
func (tm *IfaceTotalsManager) ResetAllToKernel(netMap map[string]ifaceCounters) []IfaceTotals {
tm.mu.Lock()
defer tm.mu.Unlock()
now := time.Now()
tm.m = make(map[string]*IfaceTotals, len(netMap))
out := make([]IfaceTotals, 0, len(netMap))
for iface, ctrs := range netMap {
st := &IfaceTotals{
Iface: iface,
TotalRxBytes: 0,
TotalTxBytes: 0,
LastKernelRxBytes: ctrs.RxBytes,
LastKernelTxBytes: ctrs.TxBytes,
UpdatedAt: now,
ResetAt: now,
}
tm.m[iface] = st
out = append(out, *st)
}
return out
}
func (tm *IfaceTotalsManager) Load(rows []IfaceTotals) {
tm.mu.Lock()
defer tm.mu.Unlock()
@@ -706,14 +746,27 @@ func startStatsCollector() {
}
if prevNet != nil && dt > 0 {
if prev, ok := prevNet[name]; ok {
var rxDelta, txDelta uint64
if ctrs.RxBytes >= prev.RxBytes {
rxDelta := ctrs.RxBytes - prev.RxBytes
st.RxMbps = float64(rxDelta*8) / dt / 1_000_000
rxDelta = ctrs.RxBytes - prev.RxBytes
} else {
// kernel counter reset or wrap
rxDelta = ctrs.RxBytes
}
if ctrs.TxBytes >= prev.TxBytes {
txDelta := ctrs.TxBytes - prev.TxBytes
txDelta = ctrs.TxBytes - prev.TxBytes
} else {
txDelta = ctrs.TxBytes
}
if rxDelta > 0 {
st.RxMbps = float64(rxDelta*8) / dt / 1_000_000
}
if txDelta > 0 {
st.TxMbps = float64(txDelta*8) / dt / 1_000_000
}
if statsStore != nil && (rxDelta > 0 || txDelta > 0) {
addPendingIfaceUsage(name, rxDelta, txDelta)
}
}
}
interfaces = append(interfaces, st)
@@ -746,12 +799,18 @@ func startStatsCollector() {
Interfaces: interfaces,
})
// Persist interface totals periodically (optional).
// Persist interface totals and vnstat-style usage periodically (optional).
if flushTicker != nil && statsStore != nil && ifaceTotalsMgr != nil {
select {
case <-flushTicker.C:
ctx := context.Background()
_ = statsStore.UpsertIfaceTotals(ctx, ifaceTotalsMgr.Snapshot())
if deltas := flushPendingIfaceUsage(now); len(deltas) > 0 {
if err := statsStore.UpsertIfaceUsageDeltas(ctx, deltas); err != nil {
log.Printf("vnstat usage flush failed: %v", err)
restorePendingIfaceUsage(deltas)
}
}
default:
}
}
@@ -1123,21 +1182,29 @@ func (s *Store) DeleteUser(ctx context.Context, username string) error {
// ---------- Optional persistence for interface totals ----------
func (s *Store) EnsureIfaceTotalsTable(ctx context.Context) error {
_, err := s.db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS ssh_iface_totals (
stmts := []string{
`CREATE TABLE IF NOT EXISTS ssh_iface_totals (
iface TEXT PRIMARY KEY,
total_rx_bytes BIGINT NOT NULL DEFAULT 0,
total_tx_bytes BIGINT NOT NULL DEFAULT 0,
last_kernel_rx_bytes BIGINT NOT NULL DEFAULT 0,
last_kernel_tx_bytes BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`)
return err
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
reset_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
`ALTER TABLE ssh_iface_totals ADD COLUMN IF NOT EXISTS reset_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`,
}
for _, stmt := range stmts {
if _, err := s.db.ExecContext(ctx, stmt); err != nil {
return err
}
}
return nil
}
func (s *Store) LoadIfaceTotals(ctx context.Context) ([]IfaceTotals, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT iface, total_rx_bytes, total_tx_bytes, last_kernel_rx_bytes, last_kernel_tx_bytes, updated_at
SELECT iface, total_rx_bytes, total_tx_bytes, last_kernel_rx_bytes, last_kernel_tx_bytes, updated_at, reset_at
FROM ssh_iface_totals`)
if err != nil {
return nil, err
@@ -1147,11 +1214,12 @@ func (s *Store) LoadIfaceTotals(ctx context.Context) ([]IfaceTotals, error) {
out := []IfaceTotals{}
for rows.Next() {
var r IfaceTotals
var updated time.Time
if err := rows.Scan(&r.Iface, &r.TotalRxBytes, &r.TotalTxBytes, &r.LastKernelRxBytes, &r.LastKernelTxBytes, &updated); err != nil {
var updated, resetAt time.Time
if err := rows.Scan(&r.Iface, &r.TotalRxBytes, &r.TotalTxBytes, &r.LastKernelRxBytes, &r.LastKernelTxBytes, &updated, &resetAt); err != nil {
return nil, err
}
r.UpdatedAt = updated
r.ResetAt = resetAt
out = append(out, r)
}
if err := rows.Err(); err != nil {
@@ -1166,16 +1234,21 @@ func (s *Store) UpsertIfaceTotals(ctx context.Context, rows []IfaceTotals) error
}
// Simple loop (small N: number of interfaces). Keeps CPU/DB overhead minimal.
for _, r := range rows {
resetAt := r.ResetAt
if resetAt.IsZero() {
resetAt = time.Now()
}
_, err := s.db.ExecContext(ctx, `
INSERT INTO ssh_iface_totals (iface, total_rx_bytes, total_tx_bytes, last_kernel_rx_bytes, last_kernel_tx_bytes, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW())
INSERT INTO ssh_iface_totals (iface, total_rx_bytes, total_tx_bytes, last_kernel_rx_bytes, last_kernel_tx_bytes, updated_at, reset_at)
VALUES ($1, $2, $3, $4, $5, NOW(), $6)
ON CONFLICT (iface) DO UPDATE
SET total_rx_bytes = EXCLUDED.total_rx_bytes,
total_tx_bytes = EXCLUDED.total_tx_bytes,
last_kernel_rx_bytes = EXCLUDED.last_kernel_rx_bytes,
last_kernel_tx_bytes = EXCLUDED.last_kernel_tx_bytes,
updated_at = NOW()`,
r.Iface, r.TotalRxBytes, r.TotalTxBytes, r.LastKernelRxBytes, r.LastKernelTxBytes)
updated_at = NOW(),
reset_at = EXCLUDED.reset_at`,
r.Iface, r.TotalRxBytes, r.TotalTxBytes, r.LastKernelRxBytes, r.LastKernelTxBytes, resetAt)
if err != nil {
return err
}
@@ -1213,7 +1286,11 @@ func startAdminAPI(store *Store, addr string, adminDir string) {
// Superadmin-only: server stats + DNSTT
mux.Handle("/api/stats", saSession(http.HandlerFunc(handleStats)))
mux.Handle("/api/stats/interfaces/reset", saSession(http.HandlerFunc(handleResetInterfaceStats(store))))
mux.Handle("/api/vnstat", saSession(http.HandlerFunc(handleVnstat(store))))
mux.Handle("/api/vnstat/reset", saSession(http.HandlerFunc(handleVnstatReset(store))))
mux.Handle("/api/system/logs", saSession(http.HandlerFunc(handleSystemLogs)))
mux.Handle("/api/system/logs/reset", saSession(http.HandlerFunc(handleSystemLogsReset)))
mux.Handle("/api/dnstt", saSession(http.HandlerFunc(handleDnsttStats)))
mux.Handle("/api/dnstt/logs", saSession(http.HandlerFunc(handleDnsttLogs)))
@@ -2457,6 +2534,9 @@ func main() {
} else {
startXrayClientExpiryChecker(store)
}
if err := store.EnsureIfaceUsageTables(ctx); err != nil {
log.Printf("vnstat usage tables disabled: %v", err)
}
if err := store.EnsureIfaceTotalsTable(ctx); err == nil {
rows, err2 := store.LoadIfaceTotals(ctx)
if err2 == nil {
@@ -2632,6 +2712,7 @@ func main() {
if quietLogs {
log.SetOutput(io.Discard)
}
startPanelLogLimiter()
// Initialise default per-connection bandwidth limits and SSH inactivity cleanup.
setDefaultLimits(cfg.DefaultLimitMbpsUp, cfg.DefaultLimitMbpsDown)