Skip to content

Commit 1b1f31b

Browse files
committed
feat(dashboard): implement Server-Sent Events (SSE) for real-time data
- Add SSEStream function to handle Server-Sent Events - Create Dashboard service to fetch system information - Update Dashboard handler to use SSE for real-time data - Modify dashboard template to support real-time data updates
1 parent 0336875 commit 1b1f31b

File tree

13 files changed

+309
-176
lines changed

13 files changed

+309
-176
lines changed

api/v1/v1.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package v1
22

33
import (
44
"errors"
5+
"io"
56
"net/http"
67

78
"github.com/gin-gonic/gin"
@@ -13,6 +14,27 @@ type Response struct {
1314
Data interface{} `json:"data"`
1415
}
1516

17+
func SSEStream(ctx *gin.Context, dataChan chan interface{}, eventName string) {
18+
if eventName == "" {
19+
eventName = "message"
20+
}
21+
22+
// 设置响应头
23+
ctx.Header("Content-Type", "text/event-stream")
24+
ctx.Header("Cache-Control", "no-cache")
25+
ctx.Header("Connection", "keep-alive")
26+
27+
// 使用 ctx.Stream 发送数据
28+
ctx.Stream(func(w io.Writer) bool {
29+
if data, ok := <-dataChan; ok {
30+
// 将数据作为 SSE 事件发送
31+
ctx.SSEvent(eventName, data)
32+
return true // 继续流
33+
}
34+
return false // 关闭流
35+
})
36+
}
37+
1638
func HandleSuccess(ctx *gin.Context, data interface{}) {
1739
if data == nil {
1840
data = map[string]interface{}{}

cmd/server/wire/wire.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/ch3nnn/webstack-go/internal/service"
2020
categoryService "github.com/ch3nnn/webstack-go/internal/service/category"
2121
configService "github.com/ch3nnn/webstack-go/internal/service/config"
22+
dashboardService "github.com/ch3nnn/webstack-go/internal/service/dashboard"
2223
indexService "github.com/ch3nnn/webstack-go/internal/service/index"
2324
siteService "github.com/ch3nnn/webstack-go/internal/service/site"
2425
userService "github.com/ch3nnn/webstack-go/internal/service/user"
@@ -50,6 +51,7 @@ var serviceSet = wire.NewSet(
5051
siteService.NewService,
5152
categoryService.NewService,
5253
configService.NewService,
54+
dashboardService.NewService,
5355
)
5456

5557
var serverSet = wire.NewSet(

cmd/server/wire/wire_gen.go

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @Author: chentong
3+
* @Date: 2025/02/07 19:48
4+
*/
5+
6+
package index
7+
8+
import (
9+
"time"
10+
11+
"github.com/gin-gonic/gin"
12+
"go.uber.org/zap"
13+
14+
v1 "github.com/ch3nnn/webstack-go/api/v1"
15+
)
16+
17+
func (h *Handler) Dashboard(ctx *gin.Context) {
18+
messageChan := make(chan any)
19+
go func() {
20+
for {
21+
dashboard, err := h.dashboardService.Dashboard(ctx)
22+
if err != nil {
23+
h.Logger.Error("SSE(Server-Sent Events)dashboard api", zap.Error(err))
24+
return
25+
}
26+
27+
select {
28+
case messageChan <- dashboard:
29+
default:
30+
return
31+
}
32+
33+
time.Sleep(1 * time.Second)
34+
}
35+
}()
36+
37+
v1.SSEStream(ctx, messageChan, "dashboard")
38+
}

internal/handler/dashboard/handler.go

Lines changed: 4 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -6,90 +6,15 @@
66
package index
77

88
import (
9-
"math/big"
10-
"net/http"
11-
"os"
12-
"runtime"
13-
"strings"
14-
"time"
15-
16-
"github.com/duke-git/lancet/v2/mathutil"
17-
humanize "github.com/dustin/go-humanize"
18-
"github.com/gin-gonic/gin"
19-
"github.com/shirou/gopsutil/cpu"
20-
"github.com/shirou/gopsutil/disk"
21-
"github.com/shirou/gopsutil/mem"
22-
23-
v1 "github.com/ch3nnn/webstack-go/api/v1"
249
"github.com/ch3nnn/webstack-go/internal/handler"
10+
"github.com/ch3nnn/webstack-go/internal/service/dashboard"
2511
)
2612

2713
type Handler struct {
2814
*handler.Handler
15+
dashboardService dashboard.Service
2916
}
3017

31-
func NewHandler(handler *handler.Handler) *Handler {
32-
return &Handler{Handler: handler}
33-
}
34-
35-
func (h *Handler) memory() (m mem.VirtualMemoryStat) {
36-
info, err := mem.VirtualMemory()
37-
if err != nil {
38-
return m
39-
}
40-
41-
return *info
42-
}
43-
44-
func (h *Handler) disk() (d disk.UsageStat) {
45-
info, err := disk.Usage("/")
46-
if err != nil {
47-
return d
48-
}
49-
50-
return *info
51-
}
52-
53-
func (h *Handler) cpu() (c cpu.InfoStat) {
54-
info, err := cpu.Info()
55-
if err != nil {
56-
return c
57-
}
58-
59-
if len(info) > 0 {
60-
return info[0]
61-
}
62-
63-
return c
64-
}
65-
66-
func (h *Handler) Dashboard(ctx *gin.Context) {
67-
memoryInfo := h.memory()
68-
diskInfo := h.disk()
69-
cpuInfo := h.cpu()
70-
71-
dir, _ := os.Getwd()
72-
73-
var cpuPercent float64
74-
cpuPercents, _ := cpu.Percent(time.Second, false)
75-
if len(cpuPercents) > 0 {
76-
cpuPercent = mathutil.RoundToFloat(cpuPercents[0], 2)
77-
}
78-
79-
ctx.HTML(http.StatusOK, "dashboard.html", v1.DashboardResp{
80-
ProjectVersion: "2.0",
81-
GoOS: runtime.GOOS,
82-
GoArch: runtime.GOARCH,
83-
GoVersion: runtime.Version(),
84-
ProjectPath: strings.Replace(dir, "\\", "/", -1),
85-
MemTotal: humanize.BigBytes(big.NewInt(int64(memoryInfo.Total))),
86-
MemUsed: humanize.BigBytes(big.NewInt(int64(memoryInfo.Used))),
87-
MemUsedPercent: mathutil.RoundToFloat(memoryInfo.UsedPercent, 2),
88-
DiskTotal: humanize.BigBytes(big.NewInt(int64(diskInfo.Total))),
89-
DiskUsed: humanize.BigBytes(big.NewInt(int64(diskInfo.Used))),
90-
DiskUsedPercent: mathutil.RoundToFloat(diskInfo.UsedPercent, 2),
91-
CpuName: cpuInfo.ModelName,
92-
CpuCores: cpuInfo.Cores,
93-
CpuUsedPercent: cpuPercent,
94-
})
18+
func NewHandler(handler *handler.Handler, dashboardService dashboard.Service) *Handler {
19+
return &Handler{Handler: handler, dashboardService: dashboardService}
9520
}

internal/handler/handler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import (
55
)
66

77
type Handler struct {
8-
logger *log.Logger
8+
Logger *log.Logger
99
}
1010

1111
func NewHandler(logger *log.Logger) *Handler {
1212
return &Handler{
13-
logger: logger,
13+
Logger: logger,
1414
}
1515
}

internal/middleware/jwt.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ func StrictAuth(j *jwt.JWT, logger *log.Logger) gin.HandlerFunc {
2424
}
2525
if tokenString == "" {
2626
tokenString = ctx.Query("Token")
27+
if tokenString == "" {
28+
tokenString = ctx.Query("token")
29+
}
2730
}
2831
if tokenString == "" {
2932
logger.WithContext(ctx).Warn("No token", zap.Any("data", map[string]interface{}{
@@ -62,6 +65,9 @@ func NoStrictAuth(j *jwt.JWT, logger *log.Logger) gin.HandlerFunc {
6265
}
6366
if tokenString == "" {
6467
tokenString = ctx.Query("Token")
68+
if tokenString == "" {
69+
tokenString = ctx.Query("token")
70+
}
6571
}
6672
if tokenString == "" {
6773
logger.WithContext(ctx).Warn("No token", zap.Any("data", map[string]interface{}{

internal/server/http.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ func NewHTTPServer(
8181
render.GET("", func(ctx *gin.Context) {
8282
ctx.HTML(http.StatusOK, "admin_index.html", nil)
8383
})
84-
85-
render.GET("dashboard", dashboardHandler.Dashboard)
86-
84+
render.GET("dashboard", func(ctx *gin.Context) {
85+
ctx.HTML(http.StatusOK, "dashboard.html", nil)
86+
})
8787
render.GET("modify_password", func(ctx *gin.Context) {
8888
ctx.HTML(http.StatusOK, "admin_modify_password.html", nil)
8989
})
@@ -112,6 +112,8 @@ func NewHTTPServer(
112112
// Strict permission routing group
113113
strictAuthRouter := v1.Group("/admin").Use(middleware.StrictAuth(jwt, logger))
114114
{
115+
// Dashboard
116+
strictAuthRouter.GET("/dashboard", dashboardHandler.Dashboard) // SSE(Server-Sent Events)
115117
// User
116118
strictAuthRouter.GET("/info", userHandler.Info)
117119
strictAuthRouter.POST("/logout", userHandler.Logout)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @Author: chentong
3+
* @Date: 2025/02/07 20:26
4+
*/
5+
6+
package dashboard
7+
8+
import (
9+
"math/big"
10+
"os"
11+
"runtime"
12+
"strings"
13+
"time"
14+
15+
"github.com/duke-git/lancet/v2/mathutil"
16+
humanize "github.com/dustin/go-humanize"
17+
"github.com/gin-gonic/gin"
18+
"github.com/shirou/gopsutil/cpu"
19+
"github.com/shirou/gopsutil/disk"
20+
"github.com/shirou/gopsutil/mem"
21+
22+
v1 "github.com/ch3nnn/webstack-go/api/v1"
23+
)
24+
25+
func (s *service) memory() (m mem.VirtualMemoryStat) {
26+
info, err := mem.VirtualMemory()
27+
if err != nil {
28+
return m
29+
}
30+
31+
return *info
32+
}
33+
34+
func (s *service) disk() (d disk.UsageStat) {
35+
info, err := disk.Usage("/")
36+
if err != nil {
37+
return d
38+
}
39+
40+
return *info
41+
}
42+
43+
func (s *service) cpu() (c cpu.InfoStat) {
44+
info, err := cpu.Info()
45+
if err != nil {
46+
return c
47+
}
48+
49+
if len(info) > 0 {
50+
return info[0]
51+
}
52+
53+
return c
54+
}
55+
56+
func (s *service) Dashboard(ctx *gin.Context) (*v1.DashboardResp, error) {
57+
memoryInfo := s.memory()
58+
diskInfo := s.disk()
59+
cpuInfo := s.cpu()
60+
61+
dir, _ := os.Getwd()
62+
63+
var cpuPercent float64
64+
cpuPercents, _ := cpu.Percent(time.Second, false)
65+
if len(cpuPercents) > 0 {
66+
cpuPercent = mathutil.RoundToFloat(cpuPercents[0], 2)
67+
}
68+
69+
resp := &v1.DashboardResp{
70+
ProjectVersion: "2.0",
71+
GoOS: runtime.GOOS,
72+
GoArch: runtime.GOARCH,
73+
GoVersion: runtime.Version(),
74+
ProjectPath: strings.Replace(dir, "\\", "/", -1),
75+
MemTotal: humanize.BigBytes(big.NewInt(int64(memoryInfo.Total))),
76+
MemUsed: humanize.BigBytes(big.NewInt(int64(memoryInfo.Used))),
77+
MemUsedPercent: mathutil.RoundToFloat(memoryInfo.UsedPercent, 2),
78+
DiskTotal: humanize.BigBytes(big.NewInt(int64(diskInfo.Total))),
79+
DiskUsed: humanize.BigBytes(big.NewInt(int64(diskInfo.Used))),
80+
DiskUsedPercent: mathutil.RoundToFloat(diskInfo.UsedPercent, 2),
81+
CpuName: cpuInfo.ModelName,
82+
CpuCores: cpuInfo.Cores,
83+
CpuUsedPercent: cpuPercent,
84+
}
85+
86+
return resp, nil
87+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @Author: chentong
3+
* @Date: 2025/01/17 下午7:32
4+
*/
5+
6+
package dashboard
7+
8+
import (
9+
"github.com/gin-gonic/gin"
10+
11+
v1 "github.com/ch3nnn/webstack-go/api/v1"
12+
s "github.com/ch3nnn/webstack-go/internal/service"
13+
)
14+
15+
var _ Service = (*service)(nil)
16+
17+
type Service interface {
18+
i()
19+
20+
Dashboard(ctx *gin.Context) (*v1.DashboardResp, error)
21+
}
22+
23+
type service struct {
24+
*s.Service
25+
}
26+
27+
func NewService(s *s.Service) Service {
28+
return &service{
29+
Service: s,
30+
}
31+
}
32+
33+
func (s *service) i() {}

0 commit comments

Comments
 (0)