后端更新: - 在 machines 表新增 offline_count、total_offline_seconds、last_offline_at、last_offline_reason 字段,用于记录机器离线统计信息。 - 新增 offline_logs 表,记录每次离线的开始时间、结束时间、持续时长及离线原因。 - 重写 ping 服务为状态机模式: – 单台机器依次使用 system ping / ICMP / TCP(SSH端口) 三种方式检测连通性; – 任意一次成功即视为在线; – 若一次失败,则会连续重试 3 次(间隔 2 秒),3 次均失败才判定为离线,避免网络抖动导致误判。 – 仅在状态发生 online→offline 或 offline→online 变化时更新 offline_logs 与累计时长。 – 机器按顺序逐个检测,每台间隔 30 秒。 - 新增 API:GET /admin/machines/:id/offline-logs,用于查询最近 50 条离线记录。 - CleanupLogs 增加对 offline_logs 的过期清理。 前端更新: - 机器详情页(MachineDetail.vue)新增离线统计卡片,展示离线次数、累计离线时长、上次离线原因及最近 5 条离线记录。 - 全局支持暗黑模式: – App.vue 引入 light/dark CSS 变量,并加载 Element Plus 深色主题(dark/css-vars.css)。 – MainLayout.vue 侧边栏新增主题切换按钮,支持深浅色切换并持久化到 localStorage。 – Topology.vue 监听主题变化,动态重绘 G6 节点/边的颜色、背景与阴影。 – MachineList.vue / MachineDetail.vue 全面适配深色变量(卡片、表格、标签、进度条等)。 - 机器列表卡片 UI 调整:改为顶部 OS 色点 + 在线/离线状态胶囊标签,离线卡片降低透明度并去色。 构建与部署: - 重新构建前端并打包静态资源。 - 交叉编译 Linux amd64 二进制并更新 deploy/lan-manager-debian12.tar.gz 部署包。
158 lines
3.9 KiB
Go
158 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-contrib/sessions/cookie"
|
|
"github.com/gin-gonic/gin"
|
|
"lan-manager/server/config"
|
|
"lan-manager/server/db"
|
|
"lan-manager/server/handlers"
|
|
"lan-manager/server/middleware"
|
|
"lan-manager/server/services"
|
|
)
|
|
|
|
//go:embed all:static
|
|
var webFS embed.FS
|
|
|
|
func main() {
|
|
cfg := config.Load()
|
|
|
|
if err := db.Init(cfg); err != nil {
|
|
fmt.Fprintf(os.Stderr, "db init failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
gin.SetMode(gin.ReleaseMode)
|
|
r := gin.New()
|
|
r.RedirectTrailingSlash = false
|
|
r.RedirectFixedPath = false
|
|
r.Use(gin.Recovery())
|
|
|
|
store := cookie.NewStore([]byte(cfg.SessionSecret))
|
|
store.Options(sessions.Options{
|
|
Path: "/",
|
|
MaxAge: 86400 * 7,
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
r.Use(sessions.Sessions("lanmgr_session", store))
|
|
|
|
// API routes
|
|
api := r.Group("/api")
|
|
{
|
|
authH := handlers.NewAuthHandler(cfg)
|
|
api.POST("/auth/login", authH.Login)
|
|
api.POST("/auth/logout", authH.Logout)
|
|
api.GET("/auth/me", authH.Me)
|
|
|
|
api.GET("/health", handlers.NewHealthHandler().Check)
|
|
|
|
// Guest + Admin read-only routes
|
|
guest := api.Group("/")
|
|
guest.Use(middleware.OptionalAuth())
|
|
{
|
|
mh := handlers.NewMachineHandler()
|
|
guest.GET("/machines", mh.List)
|
|
}
|
|
|
|
// Admin only routes
|
|
admin := api.Group("/")
|
|
admin.Use(middleware.AuthRequired())
|
|
{
|
|
mh := handlers.NewMachineHandler()
|
|
admin.GET("/machines/:id", mh.Get)
|
|
admin.POST("/machines", mh.Create)
|
|
admin.PUT("/machines/:id", mh.Update)
|
|
admin.DELETE("/machines/:id", mh.Delete)
|
|
admin.POST("/machines/:id/ssh-info", mh.SSHInfo)
|
|
admin.POST("/machines/:id/sync-ssh", mh.SyncSSH)
|
|
admin.GET("/machines/:id/offline-logs", mh.OfflineLogs)
|
|
|
|
sh := handlers.NewServiceHandler()
|
|
admin.GET("/machines/:id/services", sh.ListByMachine)
|
|
admin.POST("/machines/:id/services", sh.Create)
|
|
admin.PUT("/services/:id", sh.Update)
|
|
admin.DELETE("/services/:id", sh.Delete)
|
|
admin.GET("/services", sh.ListAll)
|
|
|
|
rh := handlers.NewRelationshipHandler()
|
|
admin.GET("/relationships", rh.List)
|
|
admin.POST("/relationships", rh.Create)
|
|
admin.PUT("/relationships/:id", rh.Update)
|
|
admin.DELETE("/relationships/:id", rh.Delete)
|
|
|
|
admin.GET("/logs", handlers.NewLogHandler().List)
|
|
|
|
eh := handlers.NewExportHandler()
|
|
admin.GET("/export", eh.Export)
|
|
admin.POST("/import", eh.Import)
|
|
}
|
|
}
|
|
|
|
// Static files
|
|
if cfg.WebStaticPath != "" {
|
|
r.Static("/", cfg.WebStaticPath)
|
|
r.NoRoute(func(c *gin.Context) {
|
|
c.File(cfg.WebStaticPath + "/index.html")
|
|
})
|
|
} else {
|
|
staticFS, err := fs.Sub(webFS, "static")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "static embed failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fileServer := http.FileServer(http.FS(staticFS))
|
|
r.NoRoute(func(c *gin.Context) {
|
|
path := c.Request.URL.Path
|
|
if path != "/" && path != "" && !strings.HasPrefix(path, "/assets/") && path != "/favicon.ico" {
|
|
c.Request.URL.Path = "/"
|
|
}
|
|
fileServer.ServeHTTP(c.Writer, c.Request)
|
|
})
|
|
}
|
|
|
|
// Start ping service
|
|
services.StartPingService(cfg.PingInterval)
|
|
|
|
// Start log cleanup
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
services.CleanupLogs(ctx, cfg.LogRetentionDays)
|
|
|
|
srv := &http.Server{
|
|
Addr: cfg.Host + ":" + cfg.Port,
|
|
Handler: r,
|
|
}
|
|
|
|
go func() {
|
|
fmt.Printf("Server listening on %s:%s\n", cfg.Host, cfg.Port)
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
fmt.Fprintf(os.Stderr, "server error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
fmt.Println("Shutting down server...")
|
|
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer shutdownCancel()
|
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "server shutdown error: %v\n", err)
|
|
}
|
|
fmt.Println("Server exited")
|
|
}
|