Fix panel
This commit is contained in:
@@ -9,6 +9,7 @@ let tlsForwardersState = [];
|
|||||||
let editingXrayClientId = null;
|
let editingXrayClientId = null;
|
||||||
let wzInbounds = [];
|
let wzInbounds = [];
|
||||||
let dashboardCache = { sshUsers: [], xrayInbounds: [], me: null };
|
let dashboardCache = { sshUsers: [], xrayInbounds: [], me: null };
|
||||||
|
let currentTab = "dashboard";
|
||||||
|
|
||||||
// ─── DOM refs ─────────────────────────────────────────────────────────────────
|
// ─── DOM refs ─────────────────────────────────────────────────────────────────
|
||||||
const loginOverlay = document.getElementById("loginOverlay");
|
const loginOverlay = document.getElementById("loginOverlay");
|
||||||
@@ -206,6 +207,7 @@ const tabTitles = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function selectTab(tab) {
|
function selectTab(tab) {
|
||||||
|
currentTab = tab;
|
||||||
const pane = document.getElementById("tab-" + tab);
|
const pane = document.getElementById("tab-" + tab);
|
||||||
const btn = document.querySelector(`.tab-btn[data-tab="${tab}"]`);
|
const btn = document.querySelector(`.tab-btn[data-tab="${tab}"]`);
|
||||||
if (!pane || !btn) return;
|
if (!pane || !btn) return;
|
||||||
@@ -311,14 +313,18 @@ function initAfterLogin() {
|
|||||||
selectTab("dashboard");
|
selectTab("dashboard");
|
||||||
|
|
||||||
if (currentRole === "superadmin") {
|
if (currentRole === "superadmin") {
|
||||||
loadStats();
|
loadDashboardStats();
|
||||||
statsTimer = setInterval(loadStats, 2000);
|
statsTimer = setInterval(() => {
|
||||||
|
loadDashboardStats();
|
||||||
|
if (currentTab === "stats") loadStats();
|
||||||
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
loadMe();
|
loadMe();
|
||||||
}
|
}
|
||||||
xrayTimer = setInterval(loadXrayStatus, 7000);
|
xrayTimer = setInterval(() => { loadXrayStatus(); if (currentTab === "xray") loadInbounds(); }, 7000);
|
||||||
|
|
||||||
loadUsers();
|
loadUsers();
|
||||||
|
loadXrayStatus();
|
||||||
loadInbounds();
|
loadInbounds();
|
||||||
usersTimer = setInterval(() => loadUsersSilent(), 3000);
|
usersTimer = setInterval(() => loadUsersSilent(), 3000);
|
||||||
}
|
}
|
||||||
@@ -1072,6 +1078,22 @@ async function deleteReseller(username) {
|
|||||||
// ─── Stats ────────────────────────────────────────────────────────────────────
|
// ─── Stats ────────────────────────────────────────────────────────────────────
|
||||||
document.querySelector("[data-tab='stats']")?.addEventListener("click", loadStats);
|
document.querySelector("[data-tab='stats']")?.addEventListener("click", loadStats);
|
||||||
|
|
||||||
|
async function loadDashboardStats() {
|
||||||
|
try {
|
||||||
|
const res = await api("/api/stats");
|
||||||
|
if (!res.ok) throw new Error(await res.text());
|
||||||
|
const s = await res.json();
|
||||||
|
updateDashboardStats(s);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === "auth") doAuthError();
|
||||||
|
else {
|
||||||
|
if (dashCpuVal) dashCpuVal.textContent = "erro";
|
||||||
|
if (dashRamVal) dashRamVal.textContent = "erro";
|
||||||
|
if (dashNetVal) dashNetVal.textContent = "erro";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateDashboardStats(s) {
|
function updateDashboardStats(s) {
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
const cpu = Number(s.cpu_percent ?? 0);
|
const cpu = Number(s.cpu_percent ?? 0);
|
||||||
@@ -1101,28 +1123,33 @@ function updateDashboardStats(s) {
|
|||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
const res = await api("/api/stats");
|
const res = await api("/api/stats");
|
||||||
|
if (!res.ok) throw new Error(await res.text());
|
||||||
const s = await res.json();
|
const s = await res.json();
|
||||||
updateDashboardStats(s);
|
updateDashboardStats(s);
|
||||||
const cpu = s?.cpu_percent ?? 0;
|
const cpu = Number(s?.cpu_percent ?? 0);
|
||||||
cpuVal.textContent = fmtPct(cpu);
|
if (cpuVal) cpuVal.textContent = fmtPct(cpu);
|
||||||
cpuBar.style.width = Math.min(100, Math.max(0, cpu)) + "%";
|
if (cpuBar) cpuBar.style.width = Math.min(100, Math.max(0, cpu)) + "%";
|
||||||
const mp = s?.mem_percent ?? null;
|
const mp = s?.mem_percent == null ? null : Number(s.mem_percent);
|
||||||
memVal.textContent = mp == null ? "--%" : fmtPct(mp);
|
if (memVal) memVal.textContent = mp == null ? "--%" : fmtPct(mp);
|
||||||
memBar.style.width = mp == null ? "0%" : Math.min(100, Math.max(0, mp)) + "%";
|
if (memBar) memBar.style.width = mp == null ? "0%" : Math.min(100, Math.max(0, mp)) + "%";
|
||||||
const mu = s?.mem_used_bytes, mt = s?.mem_total_bytes;
|
const mu = s?.mem_used_bytes, mt = s?.mem_total_bytes;
|
||||||
memDetail.textContent = (mu != null && mt != null) ? `${fmtBytes(mu)} / ${fmtBytes(mt)}` : "";
|
if (memDetail) memDetail.textContent = (mu != null && mt != null) ? `${fmtBytes(mu)} / ${fmtBytes(mt)}` : "";
|
||||||
const ifaces = Array.isArray(s.interfaces) ? s.interfaces : [];
|
const ifaces = Array.isArray(s.interfaces) ? s.interfaces : [];
|
||||||
ifaceBody.innerHTML = "";
|
if (ifaceBody) ifaceBody.innerHTML = "";
|
||||||
let totRx = 0, totTx = 0;
|
let totRx = 0, totTx = 0;
|
||||||
ifaces.forEach(it => {
|
ifaces.forEach(it => {
|
||||||
totRx += it.rx_bytes||0; totTx += it.tx_bytes||0;
|
totRx += Number(it.rx_bytes||0); totTx += Number(it.tx_bytes||0);
|
||||||
|
if (!ifaceBody) return;
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.innerHTML = `<td>${it.name}</td><td>${fmtMbps(it.rx_mbps)}</td><td>${fmtMbps(it.tx_mbps)}</td><td>${fmtBytes(it.rx_bytes)}</td><td>${fmtBytes(it.tx_bytes)}</td>`;
|
tr.innerHTML = `<td>${it.name}</td><td>${fmtMbps(it.rx_mbps)}</td><td>${fmtMbps(it.tx_mbps)}</td><td>${fmtBytes(it.rx_bytes)}</td><td>${fmtBytes(it.tx_bytes)}</td>`;
|
||||||
ifaceBody.appendChild(tr);
|
ifaceBody.appendChild(tr);
|
||||||
});
|
});
|
||||||
ifaceSummary.textContent = `Total: ${fmtBytes(totRx)} rx / ${fmtBytes(totTx)} tx`;
|
if (ifaceSummary) ifaceSummary.textContent = `Total: ${fmtBytes(totRx)} rx / ${fmtBytes(totTx)} tx`;
|
||||||
statsUpdated.textContent = "Updated: " + new Date().toLocaleTimeString();
|
if (statsUpdated) statsUpdated.textContent = "Updated: " + new Date().toLocaleTimeString();
|
||||||
} catch (e) { if (e.message==="auth") doAuthError(); }
|
} catch (e) {
|
||||||
|
if (e.message==="auth") doAuthError();
|
||||||
|
else if (statsUpdated) statsUpdated.textContent = "Erro ao carregar stats.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetIfaceStatsBtn?.addEventListener("click", resetInterfaceStats);
|
resetIfaceStatsBtn?.addEventListener("click", resetInterfaceStats);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<title>DragonCore Panel</title>
|
<title>DragonCore Panel</title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||||
<link rel="stylesheet" href="assets/app.css"/>
|
<link rel="stylesheet" href="assets/app.css?v=20260510xraystats2"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
@@ -876,6 +876,6 @@
|
|||||||
</div><!-- /shell -->
|
</div><!-- /shell -->
|
||||||
</div><!-- /app -->
|
</div><!-- /app -->
|
||||||
|
|
||||||
<script defer src="assets/app.js"></script>
|
<script defer src="assets/app.js?v=20260510xraystats2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
47
main.go
47
main.go
@@ -561,6 +561,48 @@ func setCurrentStats(s StatsDTO) {
|
|||||||
statsMu.Unlock()
|
statsMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// primeCurrentStats fills RAM and interface totals immediately at startup so
|
||||||
|
// the dashboard does not show placeholder values while waiting for the first
|
||||||
|
// polling interval. CPU still becomes accurate after the second /proc/stat
|
||||||
|
// sample, but it is rendered as 0.0% instead of --.
|
||||||
|
func primeCurrentStats() {
|
||||||
|
netMap, _ := readNetDev()
|
||||||
|
interfaces := make([]InterfaceStats, 0, len(netMap))
|
||||||
|
for name, ctrs := range netMap {
|
||||||
|
if isIgnoredInterface(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
st := InterfaceStats{Name: name}
|
||||||
|
if ifaceTotalsMgr != nil {
|
||||||
|
rxTotal, txTotal := ifaceTotalsMgr.ApplyKernel(name, ctrs.RxBytes, ctrs.TxBytes)
|
||||||
|
st.RxBytes = rxTotal
|
||||||
|
st.TxBytes = txTotal
|
||||||
|
} else {
|
||||||
|
st.RxBytes = ctrs.RxBytes
|
||||||
|
st.TxBytes = ctrs.TxBytes
|
||||||
|
}
|
||||||
|
interfaces = append(interfaces, st)
|
||||||
|
}
|
||||||
|
sort.Slice(interfaces, func(i, j int) bool { return interfaces[i].Name < interfaces[j].Name })
|
||||||
|
memTotal, memAvail, _ := readMemInfo()
|
||||||
|
var memUsed uint64
|
||||||
|
var memPercent float64
|
||||||
|
if memTotal > 0 {
|
||||||
|
if memAvail <= memTotal {
|
||||||
|
memUsed = memTotal - memAvail
|
||||||
|
memPercent = 100.0 * float64(memUsed) / float64(memTotal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCurrentStats(StatsDTO{
|
||||||
|
CPUPercent: 0,
|
||||||
|
MemTotal: memTotal,
|
||||||
|
MemUsed: memUsed,
|
||||||
|
MemAvail: memAvail,
|
||||||
|
MemPercent: memPercent,
|
||||||
|
Interfaces: interfaces,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type IfaceTotals struct {
|
type IfaceTotals struct {
|
||||||
Iface string
|
Iface string
|
||||||
TotalRxBytes uint64
|
TotalRxBytes uint64
|
||||||
@@ -1310,8 +1352,8 @@ func startAdminAPI(store *Store, addr string, adminDir string) {
|
|||||||
mux.Handle("/api/users/create", sessionMiddleware(http.HandlerFunc(handleCreateUser(store))))
|
mux.Handle("/api/users/create", sessionMiddleware(http.HandlerFunc(handleCreateUser(store))))
|
||||||
mux.Handle("/api/users/delete", sessionMiddleware(http.HandlerFunc(handleDeleteUser(store))))
|
mux.Handle("/api/users/delete", sessionMiddleware(http.HandlerFunc(handleDeleteUser(store))))
|
||||||
|
|
||||||
// Superadmin-only: server stats + DNSTT
|
// Server stats: visible to authenticated sessions; reset remains superadmin-only.
|
||||||
mux.Handle("/api/stats", saSession(http.HandlerFunc(handleStats)))
|
mux.Handle("/api/stats", sessionMiddleware(http.HandlerFunc(handleStats)))
|
||||||
mux.Handle("/api/stats/interfaces/reset", saSession(http.HandlerFunc(handleResetInterfaceStats(store))))
|
mux.Handle("/api/stats/interfaces/reset", saSession(http.HandlerFunc(handleResetInterfaceStats(store))))
|
||||||
mux.Handle("/api/vnstat", saSession(http.HandlerFunc(handleVnstat(store))))
|
mux.Handle("/api/vnstat", saSession(http.HandlerFunc(handleVnstat(store))))
|
||||||
mux.Handle("/api/vnstat/reset", saSession(http.HandlerFunc(handleVnstatReset(store))))
|
mux.Handle("/api/vnstat/reset", saSession(http.HandlerFunc(handleVnstatReset(store))))
|
||||||
@@ -2586,6 +2628,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start background collector for CPU + interface stats
|
// start background collector for CPU + interface stats
|
||||||
|
primeCurrentStats()
|
||||||
startStatsCollector()
|
startStatsCollector()
|
||||||
|
|
||||||
adminAddr := os.Getenv("ADMIN_HTTP_ADDR")
|
adminAddr := os.Getenv("ADMIN_HTTP_ADDR")
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ type XrayStatusDTO struct {
|
|||||||
|
|
||||||
// Status returns a snapshot of the current xray process state.
|
// Status returns a snapshot of the current xray process state.
|
||||||
func (m *XrayManager) Status() XrayStatusDTO {
|
func (m *XrayManager) Status() XrayStatusDTO {
|
||||||
|
m.refreshRuntimeStatsIfStale(3 * time.Second)
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
s := XrayStatusDTO{}
|
s := XrayStatusDTO{}
|
||||||
if m.cfg != nil {
|
if m.cfg != nil {
|
||||||
@@ -406,18 +407,35 @@ func (m *XrayManager) refreshRuntimeStats() {
|
|||||||
if m.statsByEmail == nil {
|
if m.statsByEmail == nil {
|
||||||
m.statsByEmail = make(map[string]xrayRuntimeStat, len(traffic))
|
m.statsByEmail = make(map[string]xrayRuntimeStat, len(traffic))
|
||||||
}
|
}
|
||||||
|
seen := make(map[string]bool, len(traffic))
|
||||||
for email, counters := range traffic {
|
for email, counters := range traffic {
|
||||||
|
seen[email] = true
|
||||||
prev := m.statsByEmail[email]
|
prev := m.statsByEmail[email]
|
||||||
st := xrayRuntimeStat{Email: email, Uplink: counters.Uplink, Downlink: counters.Downlink, LastActive: prev.LastActive}
|
st := xrayRuntimeStat{Email: email, Uplink: counters.Uplink, Downlink: counters.Downlink, LastActive: prev.LastActive}
|
||||||
changed := counters.Uplink != prev.Uplink || counters.Downlink != prev.Downlink
|
changed := counters.Uplink != prev.Uplink || counters.Downlink != prev.Downlink
|
||||||
// When a panel starts after Xray, the first successful poll may already
|
// First successful poll with non-zero traffic means the client has been
|
||||||
// contain non-zero per-user traffic. Treat that as recent activity so
|
// active since Xray started. Later polls refresh LastActive only when bytes
|
||||||
// online counters do not stay at zero until the next byte moves.
|
// move, which avoids counting old idle clients forever.
|
||||||
if changed && (prev.Email != "" || counters.Uplink+counters.Downlink > 0) {
|
if changed && (prev.Email != "" || counters.Uplink+counters.Downlink > 0) {
|
||||||
st.LastActive = now
|
st.LastActive = now
|
||||||
}
|
}
|
||||||
m.statsByEmail[email] = st
|
m.statsByEmail[email] = st
|
||||||
}
|
}
|
||||||
|
// Keep old entries, but do not delete them immediately. Xray may omit zero
|
||||||
|
// counters for users that have not moved traffic yet.
|
||||||
|
_ = seen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *XrayManager) refreshRuntimeStatsIfStale(maxAge time.Duration) {
|
||||||
|
if !m.isRunningSnapshot() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.statsMu.RLock()
|
||||||
|
last := m.lastStatsPoll
|
||||||
|
m.statsMu.RUnlock()
|
||||||
|
if last.IsZero() || time.Since(last) >= maxAge {
|
||||||
|
m.refreshRuntimeStats()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *XrayManager) queryUserTraffic() (map[string]xrayTrafficCounters, error) {
|
func (m *XrayManager) queryUserTraffic() (map[string]xrayTrafficCounters, error) {
|
||||||
@@ -425,16 +443,18 @@ func (m *XrayManager) queryUserTraffic() (map[string]xrayTrafficCounters, error)
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("Xray API stats not configured; add an api inbound or set xray.api_server")
|
return nil, fmt.Errorf("Xray API stats not configured; add an api inbound or set xray.api_server")
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
attempts := [][]string{
|
attempts := [][]string{
|
||||||
{"api", "statsquery", "--server=" + apiServer, "-pattern", "user>>>", "-reset=false"},
|
{"api", "statsquery", "--server=" + apiServer, "-pattern", "user>>>", "-reset=false"},
|
||||||
|
{"api", "statsquery", "--server", apiServer, "-pattern", "user>>>", "-reset=false"},
|
||||||
|
{"api", "statsquery", "--server=" + apiServer, "-pattern", "user>>>"},
|
||||||
{"api", "statsquery", "--server", apiServer, "-pattern", "user>>>"},
|
{"api", "statsquery", "--server", apiServer, "-pattern", "user>>>"},
|
||||||
}
|
}
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for _, args := range attempts {
|
for _, args := range attempts {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
cmd := exec.CommandContext(ctx, binPath, args...)
|
cmd := exec.CommandContext(ctx, binPath, args...)
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
|
cancel()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return parseXrayStatsOutput(out), nil
|
return parseXrayStatsOutput(out), nil
|
||||||
}
|
}
|
||||||
@@ -505,12 +525,17 @@ func parseXrayStatValue(raw json.RawMessage) int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addXrayCounter(result map[string]xrayTrafficCounters, name string, value int64) {
|
func addXrayCounter(result map[string]xrayTrafficCounters, name string, value int64) {
|
||||||
|
// Xray user traffic stats are normally named:
|
||||||
|
// user>>>EMAIL>>>traffic>>>uplink
|
||||||
|
// user>>>EMAIL>>>traffic>>>downlink
|
||||||
|
// Older code expected a fifth segment and therefore ignored valid Xray
|
||||||
|
// Stats API responses, which made every client look offline.
|
||||||
parts := strings.Split(name, ">>>")
|
parts := strings.Split(name, ">>>")
|
||||||
if len(parts) < 5 || parts[0] != "user" || parts[2] != "traffic" {
|
if len(parts) < 4 || parts[0] != "user" || parts[2] != "traffic" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
email := parts[1]
|
email := strings.TrimSpace(parts[1])
|
||||||
direction := parts[4]
|
direction := strings.ToLower(strings.TrimSpace(parts[3]))
|
||||||
if email == "" {
|
if email == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -527,18 +552,23 @@ func addXrayCounter(result map[string]xrayTrafficCounters, name string, value in
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *XrayManager) RuntimeStatsForEmail(email string) (xrayRuntimeStat, bool) {
|
func (m *XrayManager) RuntimeStatsForEmail(email string) (xrayRuntimeStat, bool) {
|
||||||
email = strings.TrimSpace(email)
|
return m.RuntimeStatsForKeys(email)
|
||||||
if email == "" {
|
|
||||||
return xrayRuntimeStat{}, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *XrayManager) RuntimeStatsForKeys(keys ...string) (xrayRuntimeStat, bool) {
|
||||||
m.statsMu.RLock()
|
m.statsMu.RLock()
|
||||||
st, ok := m.statsByEmail[email]
|
defer m.statsMu.RUnlock()
|
||||||
m.statsMu.RUnlock()
|
for _, key := range keys {
|
||||||
if !ok || st.Email == "" {
|
key = strings.TrimSpace(key)
|
||||||
return xrayRuntimeStat{}, false
|
if key == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
if st, ok := m.statsByEmail[key]; ok && st.Email != "" {
|
||||||
return st, true
|
return st, true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return xrayRuntimeStat{}, false
|
||||||
|
}
|
||||||
|
|
||||||
func (m *XrayManager) CountOnlineUsers() int {
|
func (m *XrayManager) CountOnlineUsers() int {
|
||||||
window := m.onlineWindow()
|
window := m.onlineWindow()
|
||||||
@@ -953,6 +983,10 @@ func ensureXrayClientEmails(raw map[string]interface{}) bool {
|
|||||||
cm["email"] = email
|
cm["email"] = email
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
if _, ok := cm["level"]; !ok {
|
||||||
|
cm["level"] = float64(0)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
seen[email]++
|
seen[email]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1066,6 +1100,18 @@ func handleXrayStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Be practical for old/manual configs: if the panel detects missing Stats API
|
||||||
|
// pieces or missing per-client stat labels, repair once automatically. This
|
||||||
|
// avoids a dashboard that says "OK" but continues to show zero Xray online
|
||||||
|
// users until a technical admin manually edits JSON.
|
||||||
|
if check, err := xrayMgr.CheckStatsAPIConfig(); err == nil && !check.Configured {
|
||||||
|
wasRunning := xrayMgr.isRunningSnapshot()
|
||||||
|
if changed, err := xrayMgr.EnsureStatsAPIConfig(); err == nil && changed && wasRunning {
|
||||||
|
if err := xrayMgr.Restart(); err != nil {
|
||||||
|
log.Printf("xray: auto stats repair restart failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
status := xrayMgr.Status()
|
status := xrayMgr.Status()
|
||||||
if sess := sessionFromCtx(r.Context()); sess != nil && sess.Role == RoleReseller && statsStore != nil {
|
if sess := sessionFromCtx(r.Context()); sess != nil && sess.Role == RoleReseller && statsStore != nil {
|
||||||
metas, err := statsStore.ListXrayClientsByOwner(r.Context(), sess.Username)
|
metas, err := statsStore.ListXrayClientsByOwner(r.Context(), sess.Username)
|
||||||
@@ -1074,7 +1120,7 @@ func handleXrayStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
window := xrayMgr.onlineWindow()
|
window := xrayMgr.onlineWindow()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for _, m := range metas {
|
for _, m := range metas {
|
||||||
st, ok := xrayMgr.RuntimeStatsForEmail(m.Email)
|
st, ok := xrayMgr.RuntimeStatsForKeys(m.Email, m.UUID, m.Name)
|
||||||
if ok && !st.LastActive.IsZero() && now.Sub(st.LastActive) <= window {
|
if ok && !st.LastActive.IsZero() && now.Sub(st.LastActive) <= window {
|
||||||
status.OnlineUsers++
|
status.OnlineUsers++
|
||||||
}
|
}
|
||||||
@@ -1453,6 +1499,7 @@ func handleXrayInbounds(w http.ResponseWriter, r *http.Request) {
|
|||||||
c.ExpiresAt = m.ExpiresAt
|
c.ExpiresAt = m.ExpiresAt
|
||||||
c.MaxConns = m.MaxConns
|
c.MaxConns = m.MaxConns
|
||||||
c.OwnerUsername = m.OwnerUsername
|
c.OwnerUsername = m.OwnerUsername
|
||||||
|
applyXrayRuntimeStats(&c)
|
||||||
if m.ExpiresAt == nil {
|
if m.ExpiresAt == nil {
|
||||||
c.ExpirationDays = -1
|
c.ExpirationDays = -1
|
||||||
} else if m.ExpiresAt.Before(now) {
|
} else if m.ExpiresAt.Before(now) {
|
||||||
@@ -1485,7 +1532,7 @@ func applyXrayRuntimeStats(c *XrayClientInfo) {
|
|||||||
if c == nil {
|
if c == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
st, ok := xrayMgr.RuntimeStatsForEmail(c.Email)
|
st, ok := xrayMgr.RuntimeStatsForKeys(c.Email, c.UUID, c.Name)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user