package main import ( "encoding/json" "html/template" "log" "net" "net/http" "os" "os/user" "runtime" "strings" "sync" "time" "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/disk" "github.com/shirou/gopsutil/v3/host" "github.com/shirou/gopsutil/v3/mem" ) type NetInterface struct { Name string `json:"name"` MAC string `json:"mac"` Addresses []string `json:"addresses"` IsLoopback bool `json:"is_loopback"` } type DiskInfo struct { Device string `json:"device"` Mountpoint string `json:"mountpoint"` Fstype string `json:"fstype"` Total uint64 `json:"total"` Used uint64 `json:"used"` Free uint64 `json:"free"` UsedPercent float64 `json:"used_percent"` ReadOnly bool `json:"read_only"` } type Summary struct { Timestamp time.Time `json:"timestamp"` Hostname string `json:"hostname"` Username string `json:"username"` OS string `json:"os"` Platform string `json:"platform"` PlatformVersion string `json:"platform_version"` KernelVersion string `json:"kernel_version"` Arch string `json:"arch"` UptimeSeconds uint64 `json:"uptime_seconds"` BootTime time.Time `json:"boot_time"` Interfaces []NetInterface `json:"interfaces"` CPUModel string `json:"cpu_model"` PhysicalCores int `json:"physical_cores"` LogicalCores int `json:"logical_cores"` CPUPercent float64 `json:"cpu_percent"` MemoryTotal uint64 `json:"memory_total"` MemoryUsed uint64 `json:"memory_used"` MemoryFree uint64 `json:"memory_free"` MemoryUsedPct float64 `json:"memory_used_percent"` SwapTotal uint64 `json:"swap_total"` SwapUsed uint64 `json:"swap_used"` SwapFree uint64 `json:"swap_free"` SwapUsedPct float64 `json:"swap_used_percent"` Disks []DiskInfo `json:"disks"` } var ( mu sync.RWMutex lastCPUUsage float64 ) // background sampler for CPU percentage to avoid blocking API calls func startCPUSampler() { go func() { // Prime once _, _ = cpu.Percent(0, false) for { pcts, err := cpu.Percent(time.Second, false) if err == nil && len(pcts) > 0 { mu.Lock() lastCPUUsage = pcts[0] mu.Unlock() } } }() } func getInterfaces() []NetInterface { ifaces, err := net.Interfaces() if err != nil { return nil } var out []NetInterface for _, ifc := range ifaces { // Skip interfaces that are down if (ifc.Flags & net.FlagUp) == 0 { continue } addrs, _ := ifc.Addrs() var ips []string isLoop := (ifc.Flags & net.FlagLoopback) != 0 for _, a := range addrs { var ip net.IP switch v := a.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip == nil { continue } if ip.IsLoopback() { continue } ips = append(ips, ip.String()) } // keep even if no IPs, to show MAC etc., but skip pure loopback if isLoop { continue } out = append(out, NetInterface{ Name: ifc.Name, MAC: ifc.HardwareAddr.String(), Addresses: ips, IsLoopback: isLoop, }) } return out } func getDisks() []DiskInfo { parts, err := disk.Partitions(true) if err != nil { return nil } var out []DiskInfo for _, p := range parts { du, err := disk.Usage(p.Mountpoint) if err != nil { continue } // read-only erkennen (gopsutil: Opts kann string ODER []string sein) ro := false switch opts := any(p.Opts).(type) { case string: ro = strings.Contains(strings.ToLower(opts), "ro") case []string: for _, o := range opts { if strings.EqualFold(o, "ro") { ro = true break } } } out = append(out, DiskInfo{ Device: p.Device, Mountpoint: p.Mountpoint, Fstype: p.Fstype, Total: du.Total, Used: du.Used, Free: du.Free, UsedPercent: du.UsedPercent, ReadOnly: ro, }) } return out } func collectSummary() Summary { hi, _ := host.Info() vm, _ := mem.VirtualMemory() sw, _ := mem.SwapMemory() coresPhys, _ := cpu.Counts(false) coresLog, _ := cpu.Counts(true) cpuInfos, _ := cpu.Info() username := "" if u, err := user.Current(); err == nil { username = u.Username } else { username = os.Getenv("USERNAME") } model := "" if len(cpuInfos) > 0 { model = cpuInfos[0].ModelName } mu.RLock() cpuPct := lastCPUUsage mu.RUnlock() var boot time.Time if hi != nil && hi.BootTime > 0 { boot = time.Unix(int64(hi.BootTime), 0) } return Summary{ Timestamp: time.Now(), Hostname: safe(hi, func(h *host.InfoStat) string { return h.Hostname }), Username: username, OS: safe(hi, func(h *host.InfoStat) string { return h.OS }), // "windows" Platform: safe(hi, func(h *host.InfoStat) string { return h.Platform }), PlatformVersion: safe(hi, func(h *host.InfoStat) string { return h.PlatformVersion }), KernelVersion: safe(hi, func(h *host.InfoStat) string { return h.KernelVersion }), Arch: runtime.GOARCH, UptimeSeconds: safe(hi, func(h *host.InfoStat) uint64 { return h.Uptime }), BootTime: boot, Interfaces: getInterfaces(), CPUModel: model, PhysicalCores: coresPhys, LogicalCores: coresLog, CPUPercent: cpuPct, MemoryTotal: vm.Total, MemoryUsed: vm.Used, MemoryFree: vm.Free, MemoryUsedPct: vm.UsedPercent, SwapTotal: sw.Total, SwapUsed: sw.Used, SwapFree: sw.Free, SwapUsedPct: sw.UsedPercent, Disks: getDisks(), } } func safe[T any, R any](v T, f func(T) R) R { if any(v) == nil { var zero R return zero } return f(v) } var page = template.Must(template.New("index").Parse(`
| Mount | Typ | Belegt | Gesamt | % |
|---|