feature: получение онлайна с каждого сервера

This commit is contained in:
Дмитрий Саенко 2025-10-22 22:09:55 +03:00
parent 971513464e
commit 9d0f2c953f
3 changed files with 148 additions and 1 deletions

View file

@ -25,6 +25,7 @@ func (c *MultiServerController) initRouter(g *gin.RouterGroup) {
g.POST("/add", c.addServer)
g.POST("/del/:id", c.delServer)
g.POST("/update/:id", c.updateServer)
g.GET("/onlines", c.getOnlineClients)
}
func (c *MultiServerController) getServers(ctx *gin.Context) {
@ -36,6 +37,15 @@ func (c *MultiServerController) getServers(ctx *gin.Context) {
jsonObj(ctx, servers, nil)
}
func (c *MultiServerController) getOnlineClients(ctx *gin.Context) {
clients, err := c.multiServerService.GetOnlineClients()
if err != nil {
jsonMsg(ctx, "Failed to get online clients", err)
return
}
jsonObj(ctx, clients, nil)
}
func (c *MultiServerController) addServer(ctx *gin.Context) {
server := &model.Server{}
err := ctx.ShouldBind(server)

View file

@ -29,6 +29,37 @@
<div class="card-tools">
<button class="btn btn-primary" @click="showAddModal">Add Server</button>
</div>
<div class="card-tools">
<template #extra>
<a-button-group>
<a-button icon="sync" @click="manualRefresh"
:loading="refreshing"></a-button>
<a-popover placement="bottomRight" trigger="click"
:overlay-class-name="themeSwitcher.currentTheme">
<template #title>
<div class="ant-custom-popover-title">
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"
size="small"></a-switch>
<span>{{ i18n "pages.inbounds.autoRefresh" }}</span>
</div>
</template>
<template #content>
<a-space direction="vertical">
<span>{{ i18n "pages.inbounds.autoRefreshInterval" }}</span>
<a-select v-model="refreshInterval"
:disabled="!isRefreshEnabled" :style="{ width: '100%' }"
@change="changeRefreshInterval"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in [5,10,30,60]"
:value="key*1000">[[ key ]]s</a-select-option>
</a-select>
</a-space>
</template>
<a-button icon="down"></a-button>
</a-popover>
</a-button-group>
</template>
</div>
</div>
<div class="card-body">
<table class="table table-bordered">
@ -50,6 +81,31 @@
<td>[[ server.name ]]</td>
<td>[[ server.address ]]</td>
<td>[[ server.port ]]</td>
<td>
<a-col :sm="12" :md="5">
<a-custom-statistic
title='{{ i18n "pages.inbounds.onlineClients" }}'
:value="onlineClients.length"
:style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix>
<a-icon type="team"></a-icon>
</template>
<template #suffix>
<a-popover title='{{ i18n "online" }}'
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<div v-for="clientEmail in onlineClients">
<span>[[ clientEmail ]]</span>
</div>
</template>
<a-tag color="blue"
v-if="onlineClients.length">[[
onlineClients.length ]]</a-tag>
</a-popover>
</template>
</a-custom-statistic>
</a-col>
</td>
<td><span v-if="server.enable"
class="badge bg-success">Yes</span><span v-else
class="badge bg-danger">No</span>
@ -154,6 +210,10 @@
el: "#app",
data: {
themeSwitcher,
onlineClients: [],
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
loadingStates: {
fetched: false,
spinning: false,
@ -173,18 +233,42 @@
},
methods: {
loadServers() {
axios.get('/panel/api/servers/list')
.then(response => {
this.servers = response.data.obj;
this.servers = servers.map(s => {
s.status = 'Checking...';
return s;
});
if (this.servers.length == 0) {
}
this.checkStatuses();
})
.catch(error => {
alert(error);
});
},
async manualRefresh() {
await this.getOnlineUsers();
},
async getOnlineUsers() {
const msg = await HttpUtil.post('/panel/api/servers/onlines');
if (!msg.success) {
return;
}
this.onlineClients = msg.obj != null ? msg.obj : [];
},
toggleRefresh() {
localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
},
changeRefreshInterval() {
localStorage.setItem("refreshInterval", this.refreshInterval);
},
showAddModal() {
this.modal.title = "Add Server";
this.modal.server = {
@ -208,6 +292,16 @@
const modal = new bootstrap.Modal(modalEl);
modal.show();
},
async startDataRefreshLoop() {
while (this.isRefreshEnabled) {
try {
await this.getOnlineUsers();
} catch (e) {
console.error(e);
}
await PromiseUtil.sleep(this.refreshInterval);
}
},
saveServer() {
let url = "/panel/api/servers/add";
if (this.modal.server.id) {
@ -286,6 +380,12 @@
mounted() {
this.loadServers();
if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
else {
this.getDBInbounds();
}
},
});
</script>

View file

@ -1,6 +1,10 @@
package service
import (
"encoding/json"
"fmt"
"net/http"
"github.com/mhsanaei/3x-ui/v2/database"
"github.com/mhsanaei/3x-ui/v2/database/model"
)
@ -21,6 +25,39 @@ func (s *MultiServerService) GetServer(id int) (*model.Server, error) {
return &server, err
}
// GetOnlineClients
func (s *MultiServerService) GetOnlineClients() (map[int][]string, error) {
db := database.GetDB()
var servers []*model.Server
err := db.Find(&servers).Error
if err != nil {
return nil, err
}
var clients map[int][]string
for _, server := range servers {
var onlineResp struct {
Success bool `json:"success"`
Msg string `json:"msg"`
Obj []string `json:"obj"`
}
url := fmt.Sprintf("http://%s:%d%spanel/api/inbounds/onlines", server.Address, server.Port, server.SecretWebPath)
resp, err := http.Post(url, "application/json", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&onlineResp); err != nil {
return nil, fmt.Errorf("decode online: %w", err)
}
if !onlineResp.Success {
return nil, fmt.Errorf("failed to get online list at %s", server.Address)
}
clients[server.Id] = onlineResp.Obj
}
return clients, nil
}
func (s *MultiServerService) AddServer(server *model.Server) error {
db := database.GetDB()
return db.Create(server).Error