Backend: - New /api/server/health endpoint returning server, paths, disk, mods, steamcmd, and frontend status - Health endpoint checks directory existence/writability, disk usage, server binary, and frontend dist availability Frontend: - New Status page (route /status) with sections for Server, SteamCMD, Mods, Frontend, Directories (with exists/writable badges), and Disk Usage (with size formatting and used-percent coloring) - Paths are clickable to copy and use start-truncation (ellipsis on the left) with native hover tooltip - Health API client integration with 10s auto-refresh Misc: - Update .gitignore and Makefile, minor Layout nav addition
189 lines
4.0 KiB
Go
189 lines
4.0 KiB
Go
package api
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"syscall"
|
|
|
|
"arma3-web-server/internal/services"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
type HealthResponse struct {
|
|
Server ServerHealth `json:"server"`
|
|
Paths []PathHealth `json:"paths"`
|
|
Mods ModsHealth `json:"mods"`
|
|
Disk []DiskHealth `json:"disk"`
|
|
SteamCMD SteamCMDHealth `json:"steamcmd"`
|
|
Frontend FrontendHealth `json:"frontend"`
|
|
}
|
|
|
|
type ServerHealth struct {
|
|
Running bool `json:"running"`
|
|
Installed bool `json:"installed"`
|
|
BinaryPath string `json:"binary_path,omitempty"`
|
|
}
|
|
|
|
type PathHealth struct {
|
|
Path string `json:"path"`
|
|
Exists bool `json:"exists"`
|
|
Writable bool `json:"writable"`
|
|
}
|
|
|
|
type ModsHealth struct {
|
|
WorkshopCount int `json:"workshop_count"`
|
|
LocalCount int `json:"local_count"`
|
|
}
|
|
|
|
type DiskHealth struct {
|
|
Path string `json:"path"`
|
|
TotalBytes uint64 `json:"total_bytes"`
|
|
FreeBytes uint64 `json:"free_bytes"`
|
|
UsedPercent float64 `json:"used_percent"`
|
|
}
|
|
|
|
type SteamCMDHealth struct {
|
|
Running bool `json:"running"`
|
|
}
|
|
|
|
type FrontendHealth struct {
|
|
Served bool `json:"served"`
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
func (h *Handler) Health(c *gin.Context) {
|
|
workshopMods := services.ListWorkshopMods(h.serverfileDir)
|
|
localMods := services.ListLocalMods(h.modsDir)
|
|
|
|
paths := h.checkPaths()
|
|
disk := h.checkDisk()
|
|
frontendServed, _ := h.checkFrontend()
|
|
|
|
resp := HealthResponse{
|
|
Server: ServerHealth{
|
|
Running: h.process.IsRunning(),
|
|
Installed: checkServerBinary(h.serverfileDir),
|
|
BinaryPath: serverBinaryPath(h.serverfileDir),
|
|
},
|
|
Paths: paths,
|
|
Mods: ModsHealth{
|
|
WorkshopCount: len(workshopMods),
|
|
LocalCount: len(localMods),
|
|
},
|
|
Disk: disk,
|
|
SteamCMD: SteamCMDHealth{
|
|
Running: h.steamcmd.IsRunning(),
|
|
},
|
|
Frontend: FrontendHealth{
|
|
Served: frontendServed,
|
|
Path: h.frontendDir,
|
|
},
|
|
}
|
|
|
|
c.JSON(200, resp)
|
|
}
|
|
|
|
func (h *Handler) checkPaths() []PathHealth {
|
|
dirs := []string{
|
|
h.dataDir,
|
|
filepath.Join(h.dataDir, "modlists"),
|
|
h.serverfileDir,
|
|
h.modsDir,
|
|
h.cfgDir,
|
|
h.profilesDir,
|
|
}
|
|
|
|
result := make([]PathHealth, 0, len(dirs))
|
|
for _, dir := range dirs {
|
|
p := PathHealth{Path: dir}
|
|
fi, err := os.Stat(dir)
|
|
if err == nil && fi.IsDir() {
|
|
p.Exists = true
|
|
p.Writable = isWritable(dir)
|
|
}
|
|
result = append(result, p)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (h *Handler) checkDisk() []DiskHealth {
|
|
paths := []string{h.dataDir, h.serverfileDir}
|
|
seen := make(map[string]bool)
|
|
var result []DiskHealth
|
|
|
|
for _, p := range paths {
|
|
info, err := diskUsage(p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
key := info.Path
|
|
if seen[key] {
|
|
continue
|
|
}
|
|
seen[key] = true
|
|
result = append(result, info)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (h *Handler) checkFrontend() (bool, string) {
|
|
if h.frontendDir == "" {
|
|
return false, ""
|
|
}
|
|
fi, err := os.Stat(h.frontendDir)
|
|
return err == nil && fi.IsDir(), h.frontendDir
|
|
}
|
|
|
|
func checkServerBinary(serverfileDir string) bool {
|
|
_, found := findServerBinary(serverfileDir)
|
|
return found
|
|
}
|
|
|
|
func serverBinaryPath(serverfileDir string) string {
|
|
path, _ := findServerBinary(serverfileDir)
|
|
return path
|
|
}
|
|
|
|
func findServerBinary(serverfileDir string) (string, bool) {
|
|
candidates := []string{"arma3server_x64"}
|
|
if runtime.GOOS == "windows" {
|
|
candidates = append(candidates, "arma3server_x64.exe")
|
|
} else {
|
|
candidates = append(candidates, "arma3server_x64.exe")
|
|
}
|
|
for _, name := range candidates {
|
|
p := filepath.Join(serverfileDir, name)
|
|
fi, err := os.Stat(p)
|
|
if err == nil && !fi.IsDir() {
|
|
return p, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func isWritable(path string) bool {
|
|
return unix.Access(path, unix.W_OK) == nil
|
|
}
|
|
|
|
func diskUsage(path string) (DiskHealth, error) {
|
|
var stat syscall.Statfs_t
|
|
if err := syscall.Statfs(path, &stat); err != nil {
|
|
return DiskHealth{}, err
|
|
}
|
|
total := stat.Blocks * uint64(stat.Bsize)
|
|
free := stat.Bfree * uint64(stat.Bsize)
|
|
var usedPercent float64
|
|
if total > 0 {
|
|
usedPercent = float64(total-free) / float64(total) * 100
|
|
}
|
|
return DiskHealth{
|
|
Path: path,
|
|
TotalBytes: total,
|
|
FreeBytes: free,
|
|
UsedPercent: usedPercent,
|
|
}, nil
|
|
}
|