Skip to content

Commit 7249efc

Browse files
committed
improve the formatting
1 parent c65dcb8 commit 7249efc

File tree

5 files changed

+188
-2
lines changed

5 files changed

+188
-2
lines changed

cmd/agent/main.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ func main() {
148148
User: agent.user,
149149
Timestamp: time.Now(),
150150
Checks: agent.runAllChecks(ctx),
151+
OS: agent.osInfo(ctx),
152+
Architecture: agent.architecture(ctx),
153+
Version: agent.osVersion(ctx),
151154
SystemUptime: agent.systemUptime(ctx),
152155
CPULoad: agent.cpuLoad(ctx),
153156
LoggedInUsers: agent.loggedInUsers(ctx),
@@ -268,6 +271,9 @@ func (a *Agent) reportToServer(ctx context.Context) {
268271
User: a.user,
269272
Timestamp: time.Now(),
270273
Checks: a.runAllChecks(ctx),
274+
OS: a.osInfo(ctx),
275+
Architecture: a.architecture(ctx),
276+
Version: a.osVersion(ctx),
271277
SystemUptime: uptime,
272278
CPULoad: cpuLoad,
273279
LoggedInUsers: loggedInUsers,
@@ -576,6 +582,13 @@ func (*Agent) cpuLoad(ctx context.Context) string {
576582

577583
if output, err := cmd.Output(); err == nil {
578584
result := strings.TrimSpace(string(output))
585+
// For Linux /proc/loadavg, extract just the three load averages
586+
if runtime.GOOS == "linux" {
587+
fields := strings.Fields(result)
588+
if len(fields) >= 3 {
589+
result = strings.Join(fields[:3], " ")
590+
}
591+
}
579592
// For uptime output on Solaris, extract just the load average part
580593
if (runtime.GOOS == "solaris" || runtime.GOOS == "illumos") && strings.Contains(result, "load average:") {
581594
parts := strings.Split(result, "load average:")
@@ -628,6 +641,77 @@ func (*Agent) loggedInUsers(ctx context.Context) string {
628641
return "unavailable"
629642
}
630643

644+
func (*Agent) osInfo(ctx context.Context) string {
645+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
646+
defer cancel()
647+
var cmd *exec.Cmd
648+
switch runtime.GOOS {
649+
case "linux":
650+
// Try to get pretty name from os-release
651+
if data, err := os.ReadFile("/etc/os-release"); err == nil {
652+
lines := strings.Split(string(data), "\n")
653+
for _, line := range lines {
654+
if strings.HasPrefix(line, "PRETTY_NAME=") {
655+
name := strings.TrimPrefix(line, "PRETTY_NAME=")
656+
return strings.Trim(name, `"`)
657+
}
658+
}
659+
}
660+
// Fallback to uname
661+
cmd = exec.CommandContext(ctx, "uname", "-s")
662+
case "darwin":
663+
cmd = exec.CommandContext(ctx, "sw_vers", "-productName")
664+
case "windows":
665+
cmd = exec.CommandContext(ctx, "wmic", "os", "get", "Caption", "/value")
666+
default:
667+
cmd = exec.CommandContext(ctx, "uname", "-s")
668+
}
669+
if cmd != nil {
670+
if output, err := cmd.Output(); err == nil {
671+
result := strings.TrimSpace(string(output))
672+
if runtime.GOOS == "windows" && strings.Contains(result, "Caption=") {
673+
result = strings.TrimPrefix(result, "Caption=")
674+
}
675+
if result != "" {
676+
return result
677+
}
678+
}
679+
}
680+
// Fallback to Go's runtime info
681+
return runtime.GOOS
682+
}
683+
684+
func (*Agent) architecture(ctx context.Context) string {
685+
// runtime.GOARCH is the most reliable way to get architecture
686+
return runtime.GOARCH
687+
}
688+
689+
func (*Agent) osVersion(ctx context.Context) string {
690+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
691+
defer cancel()
692+
var cmd *exec.Cmd
693+
switch runtime.GOOS {
694+
case "linux":
695+
cmd = exec.CommandContext(ctx, "uname", "-r")
696+
case "darwin":
697+
cmd = exec.CommandContext(ctx, "sw_vers", "-productVersion")
698+
case "windows":
699+
cmd = exec.CommandContext(ctx, "wmic", "os", "get", "Version", "/value")
700+
default:
701+
cmd = exec.CommandContext(ctx, "uname", "-r")
702+
}
703+
if output, err := cmd.Output(); err == nil {
704+
result := strings.TrimSpace(string(output))
705+
if runtime.GOOS == "windows" && strings.Contains(result, "Version=") {
706+
result = strings.TrimPrefix(result, "Version=")
707+
}
708+
if result != "" {
709+
return result
710+
}
711+
}
712+
return "unknown"
713+
}
714+
631715
func darwinHardwareID() string {
632716
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
633717
defer cancel()

cmd/server/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,9 @@ func (s *Server) handleReport(writer http.ResponseWriter, request *http.Request)
801801
LastIP: clientIP,
802802
Checks: report.Checks,
803803
// In-memory only fields
804+
OS: report.OS,
805+
Architecture: report.Architecture,
806+
Version: report.Version,
804807
SystemUptime: report.SystemUptime,
805808
CPULoad: report.CPULoad,
806809
LoggedInUsers: report.LoggedInUsers,

cmd/server/templates/device.html

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,44 @@ <h1 class="page-title">{{.Hostname}}</h1>
431431
</svg>
432432
{{.User}}
433433
</div>
434+
{{if and .OS .Version}}
435+
<div class="subtitle-item">
436+
<svg class="status-icon" viewBox="0 0 16 16" fill="currentColor">
437+
<path d="M2.5 1A1.5 1.5 0 0 0 1 2.5v11A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-11A1.5 1.5 0 0 0 13.5 1h-11ZM3 3.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3 0a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5ZM3 7a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8A.5.5 0 0 1 3 7Zm0 2a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8A.5.5 0 0 1 3 9Zm0 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5A.5.5 0 0 1 3 11Z"/>
438+
</svg>
439+
{{.OS}} {{.Version}} ({{.Architecture}})
440+
</div>
441+
{{else if .OS}}
442+
<div class="subtitle-item">
443+
<svg class="status-icon" viewBox="0 0 16 16" fill="currentColor">
444+
<path d="M2.5 1A1.5 1.5 0 0 0 1 2.5v11A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-11A1.5 1.5 0 0 0 13.5 1h-11ZM3 3.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3 0a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5ZM3 7a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8A.5.5 0 0 1 3 7Zm0 2a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8A.5.5 0 0 1 3 9Zm0 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5A.5.5 0 0 1 3 11Z"/>
445+
</svg>
446+
{{.OS}}
447+
</div>
448+
{{end}}
449+
{{if .FormattedUptime}}
450+
<div class="subtitle-item">
451+
<svg class="status-icon" viewBox="0 0 16 16" fill="currentColor">
452+
<path d="M6.5 1A.5.5 0 0 1 7 .5h2a.5.5 0 0 1 0 1v.57c1.36.196 2.594.78 3.584 1.64l.403-.403a.5.5 0 0 1 .707 0l1.414 1.414a.5.5 0 0 1 0 .707l-.403.403c.86.99 1.444 2.224 1.64 3.584H15a.5.5 0 0 1 0 1v-2a.5.5 0 0 1 0 1h-.57c-.196 1.36-.78 2.594-1.64 3.584l.403.403a.5.5 0 0 1 0 .707l-1.414 1.414a.5.5 0 0 1-.707 0l-.403-.403c-.99.86-2.224 1.444-3.584 1.64V15.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1v-.57c-1.36-.196-2.594-.78-3.584-1.64l-.403.403a.5.5 0 0 1-.707 0L.793 12.207a.5.5 0 0 1 0-.707l.403-.403C.336 10.107-.248 8.873-.444 7.513H1a.5.5 0 0 1 0-1h2a.5.5 0 0 1 0 1h.57c.196-1.36.78-2.594 1.64-3.584L3.393 3.393a.5.5 0 0 1 0-.707L4.807.793a.5.5 0 0 1 .707 0l.403.403C6.907.336 8.141-.248 9.501-.444V1a.5.5 0 0 1-.5.5ZM8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2Z"/>
453+
</svg>
454+
Up {{.FormattedUptime}}
455+
</div>
456+
{{else if .SystemUptime}}
457+
<div class="subtitle-item">
458+
<svg class="status-icon" viewBox="0 0 16 16" fill="currentColor">
459+
<path d="M6.5 1A.5.5 0 0 1 7 .5h2a.5.5 0 0 1 0 1v.57c1.36.196 2.594.78 3.584 1.64l.403-.403a.5.5 0 0 1 .707 0l1.414 1.414a.5.5 0 0 1 0 .707l-.403.403c.86.99 1.444 2.224 1.64 3.584H15a.5.5 0 0 1 0 1v-2a.5.5 0 0 1 0 1h-.57c-.196 1.36-.78 2.594-1.64 3.584l.403.403a.5.5 0 0 1 0 .707l-1.414 1.414a.5.5 0 0 1-.707 0l-.403-.403c-.99.86-2.224 1.444-3.584 1.64V15.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1v-.57c-1.36-.196-2.594-.78-3.584-1.64l-.403.403a.5.5 0 0 1-.707 0L.793 12.207a.5.5 0 0 1 0-.707l.403-.403C.336 10.107-.248 8.873-.444 7.513H1a.5.5 0 0 1 0-1h2a.5.5 0 0 1 0 1h.57c.196-1.36.78-2.594 1.64-3.584L3.393 3.393a.5.5 0 0 1 0-.707L4.807.793a.5.5 0 0 1 .707 0l.403.403C6.907.336 8.141-.248 9.501-.444V1a.5.5 0 0 1-.5.5ZM8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2Z"/>
460+
</svg>
461+
Uptime: {{.SystemUptime}}
462+
</div>
463+
{{end}}
464+
{{if .CPULoad}}
465+
<div class="subtitle-item">
466+
<svg class="status-icon" viewBox="0 0 16 16" fill="currentColor">
467+
<path d="M5 3a5 5 0 0 0 0 10h6a5 5 0 0 0 0-10H5Zm6 9a4 4 0 1 1 0-8 4 4 0 0 1 0 8Z"/>
468+
</svg>
469+
Load: {{.CPULoad}}
470+
</div>
471+
{{end}}
434472
<div class="subtitle-item">
435473
<svg class="status-icon" viewBox="0 0 16 16" fill="currentColor">
436474
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7-3.25v2.992l2.028.812a.75.75 0 0 1-.557 1.392l-2.5-1A.751.751 0 0 1 7 8.25v-3.5a.75.75 0 0 1 1.5 0Z"/>

internal/gitmdm/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ type Device struct {
1212
Hostname string `json:"hostname"`
1313
User string `json:"user"`
1414
// In-memory only fields (not persisted to git)
15+
OS string `json:"-"`
16+
Architecture string `json:"-"`
17+
Version string `json:"-"`
1518
SystemUptime string `json:"-"`
1619
CPULoad string `json:"-"`
1720
LoggedInUsers string `json:"-"`
@@ -47,6 +50,9 @@ type DeviceReport struct {
4750
HardwareID string `json:"hardware_id"`
4851
Hostname string `json:"hostname"`
4952
User string `json:"user"`
53+
OS string `json:"os,omitempty"`
54+
Architecture string `json:"architecture,omitempty"`
55+
Version string `json:"version,omitempty"`
5056
SystemUptime string `json:"system_uptime,omitempty"`
5157
CPULoad string `json:"cpu_load,omitempty"`
5258
LoggedInUsers string `json:"logged_in_users,omitempty"`

internal/viewmodels/viewmodels.go

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package viewmodels
33

44
import (
55
"gitmdm/internal/gitmdm"
6+
"regexp"
67
"sort"
8+
"strconv"
79
"strings"
810
"time"
911
)
@@ -56,6 +58,7 @@ type DeviceDetail struct {
5658
PassCount int
5759
FailCount int
5860
NACount int
61+
FormattedUptime string // Human-readable uptime
5962
}
6063

6164
// CheckResult represents the display info for a check.
@@ -66,12 +69,64 @@ type CheckResult struct {
6669
Remediation []string
6770
}
6871

72+
// formatUptime parses various uptime formats and returns a human-readable string.
73+
func formatUptime(uptime string) string {
74+
if uptime == "" || uptime == "unavailable" || uptime == "unsupported" {
75+
return uptime
76+
}
77+
78+
// Try to parse macOS/Linux uptime format (e.g., "17:05 up 35 mins, 3 users, load averages: 2.68 2.93 2.87")
79+
uptimeRegex := regexp.MustCompile(`up\s+(?:(\d+)\s+days?,\s*)?(?:(\d+):(\d+),|(\d+)\s+mins?)`)
80+
matches := uptimeRegex.FindStringSubmatch(uptime)
81+
82+
if len(matches) > 0 {
83+
days := 0
84+
hours := 0
85+
mins := 0
86+
87+
if matches[1] != "" {
88+
days, _ = strconv.Atoi(matches[1])
89+
}
90+
if matches[2] != "" {
91+
hours, _ = strconv.Atoi(matches[2])
92+
}
93+
if matches[3] != "" {
94+
mins, _ = strconv.Atoi(matches[3])
95+
}
96+
if matches[4] != "" {
97+
mins, _ = strconv.Atoi(matches[4])
98+
}
99+
100+
// Format the output
101+
if days > 0 {
102+
if days == 1 {
103+
return "1 day"
104+
}
105+
return strconv.Itoa(days) + " days"
106+
} else if hours > 0 {
107+
if hours == 1 {
108+
return "1 hour"
109+
}
110+
return strconv.Itoa(hours) + " hours"
111+
} else if mins > 0 {
112+
if mins == 1 {
113+
return "1 minute"
114+
}
115+
return strconv.Itoa(mins) + " minutes"
116+
}
117+
}
118+
119+
// Try to parse Windows format or if the regex didn't match, return the original
120+
return uptime
121+
}
122+
69123
// BuildDeviceDetail creates a detailed view model for a single device.
70124
// staleThreshold determines which checks to include - zero time means include all.
71125
func BuildDeviceDetail(device *gitmdm.Device, staleThreshold time.Time) *DeviceDetail {
72126
detail := &DeviceDetail{
73-
Device: device,
74-
CheckResults: make(map[string]CheckResult),
127+
Device: device,
128+
CheckResults: make(map[string]CheckResult),
129+
FormattedUptime: formatUptime(device.SystemUptime),
75130
}
76131

77132
// Process all checks

0 commit comments

Comments
 (0)