All checks were successful
Beta Release / beta (push) Successful in 46s
- Rewrite dashboard from 4 tabs to single grid view with 5s auto-refresh - Add live CPU/RAM/Network SVG graphs with rolling 30-point history - Add backend /api/system/metrics reading /proc/stat, /proc/meminfo, /proc/net/dev - Add backend /api/providers/quota for MiniMax and Z.AI quota monitoring - Add backend /api/recent-commands reading bash/zsh history - Add backend /api/running-processes filtering editors/IDEs/languages - Add sudo/root indicator (⚡ ROOT) in footer when running as root - Remove duplicate Ctrl+1-4 shortcut from page-specific footer (keep only right side) - Add Ctrl+R shortcut on dashboard for metrics-only refresh - Make API key mandatory in onboarding, auto-scan editors via AI chat - Remove manual editor input, only show AI-detected editors - Bump version to 0.3.3 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
721 lines
18 KiB
Go
721 lines
18 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/muyue/muyue/internal/lsp"
|
|
"github.com/muyue/muyue/internal/mcp"
|
|
"github.com/muyue/muyue/internal/scanner"
|
|
"github.com/muyue/muyue/internal/skills"
|
|
"github.com/muyue/muyue/internal/version"
|
|
)
|
|
|
|
func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, map[string]interface{}{
|
|
"name": version.Name,
|
|
"version": version.Version,
|
|
"author": version.Author,
|
|
"sudo": os.Geteuid() == 0,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleSystem(w http.ResponseWriter, r *http.Request) {
|
|
if s.scanResult == nil {
|
|
s.scanResult = scanner.ScanSystem()
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"system": s.scanResult.System,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleTools(w http.ResponseWriter, r *http.Request) {
|
|
if s.scanResult == nil {
|
|
s.scanResult = scanner.ScanSystem()
|
|
}
|
|
type toolInfo struct {
|
|
Name string `json:"name"`
|
|
Installed bool `json:"installed"`
|
|
Version string `json:"version"`
|
|
Path string `json:"path"`
|
|
}
|
|
tools := make([]toolInfo, len(s.scanResult.Tools))
|
|
for i, t := range s.scanResult.Tools {
|
|
tools[i] = toolInfo{
|
|
Name: t.Name,
|
|
Installed: t.Installed,
|
|
Version: t.Version,
|
|
Path: t.Path,
|
|
}
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"tools": tools,
|
|
"total": len(tools),
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
|
if s.config == nil {
|
|
writeError(w, "no config", http.StatusNotFound)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"profile": s.config.Profile,
|
|
"terminal": s.config.Terminal,
|
|
"bmad": s.config.BMAD,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleProviders(w http.ResponseWriter, r *http.Request) {
|
|
if s.config == nil {
|
|
writeError(w, "no config", http.StatusNotFound)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"providers": s.config.AI.Providers,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleSkills(w http.ResponseWriter, r *http.Request) {
|
|
list, err := skills.List()
|
|
if err != nil {
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"skills": list,
|
|
"count": len(list),
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleLSP(w http.ResponseWriter, r *http.Request) {
|
|
servers := lsp.ScanServers()
|
|
writeJSON(w, map[string]interface{}{
|
|
"servers": servers,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleMCP(w http.ResponseWriter, r *http.Request) {
|
|
servers := mcp.ScanServers()
|
|
home, _ := os.UserHomeDir()
|
|
editors := mcp.DetectInstalledEditors(home)
|
|
statuses := mcp.GetAllStatuses()
|
|
writeJSON(w, map[string]interface{}{
|
|
"servers": servers,
|
|
"configured": true,
|
|
"detected_editors": editors,
|
|
"statuses": statuses,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleMCPConfigure(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
Editor string `json:"editor,omitempty"`
|
|
}
|
|
if r.Body != nil {
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
}
|
|
|
|
if body.Editor != "" {
|
|
if err := mcp.ConfigureForEditor(s.config, body.Editor); err != nil {
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
} else {
|
|
if err := mcp.ConfigureAll(s.config); err != nil {
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
writeJSON(w, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (s *Server) handleMCPStatus(w http.ResponseWriter, r *http.Request) {
|
|
statuses := mcp.GetAllStatuses()
|
|
writeJSON(w, map[string]interface{}{
|
|
"statuses": statuses,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleMCPRegistry(w http.ResponseWriter, r *http.Request) {
|
|
reg, err := mcp.LoadRegistry()
|
|
if err != nil {
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"registry": reg,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleLSPHealth(w http.ResponseWriter, r *http.Request) {
|
|
servers := lsp.ScanServers()
|
|
type healthInfo struct {
|
|
Name string `json:"name"`
|
|
Language string `json:"language"`
|
|
Installed bool `json:"installed"`
|
|
Healthy bool `json:"healthy"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
var results []healthInfo
|
|
for _, srv := range servers {
|
|
healthy, detail := lsp.HealthCheck(srv.Name)
|
|
results = append(results, healthInfo{
|
|
Name: srv.Name,
|
|
Language: srv.Language,
|
|
Installed: srv.Installed,
|
|
Healthy: healthy,
|
|
Detail: detail,
|
|
})
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"servers": results,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleLSPAutoInstall(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
ProjectDir string `json:"project_dir,omitempty"`
|
|
}
|
|
if r.Body != nil {
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
}
|
|
|
|
if body.ProjectDir == "" {
|
|
home, _ := os.UserHomeDir()
|
|
body.ProjectDir = home
|
|
}
|
|
|
|
results, err := lsp.AutoInstallForProject(body.ProjectDir)
|
|
if err != nil {
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"results": results,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleLSPEditorConfig(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
Editor string `json:"editor"`
|
|
Names []string `json:"names,omitempty"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
allServers := lsp.ScanServers()
|
|
var selected []lsp.LSPServer
|
|
if len(body.Names) > 0 {
|
|
nameSet := map[string]bool{}
|
|
for _, n := range body.Names {
|
|
nameSet[n] = true
|
|
}
|
|
for _, srv := range allServers {
|
|
if nameSet[srv.Name] {
|
|
selected = append(selected, srv)
|
|
}
|
|
}
|
|
} else {
|
|
for _, srv := range allServers {
|
|
if srv.Installed {
|
|
selected = append(selected, srv)
|
|
}
|
|
}
|
|
}
|
|
|
|
config, err := lsp.GenerateEditorConfigs(selected, body.Editor, "")
|
|
if err != nil {
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, map[string]interface{}{
|
|
"editor": body.Editor,
|
|
"config": config,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleSkillValidate(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
skill, err := skills.Get(body.Name)
|
|
if err != nil {
|
|
writeError(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
errs := skills.Validate(skill)
|
|
writeJSON(w, map[string]interface{}{
|
|
"name": body.Name,
|
|
"valid": len(errs) == 0,
|
|
"errors": errs,
|
|
"dependencies": skills.CheckDependencies(skill),
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleSkillTest(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
SampleTask string `json:"sample_task,omitempty"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
result := skills.DryRun(body.Name, body.SampleTask)
|
|
writeJSON(w, result)
|
|
}
|
|
|
|
func (s *Server) handleSkillExport(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
ExportPath string `json:"export_path"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
home, _ := os.UserHomeDir()
|
|
if body.ExportPath == "" {
|
|
body.ExportPath = home + "/.muyue/exports/" + body.Name + ".md"
|
|
}
|
|
|
|
if err := skills.Export(body.Name, body.ExportPath); err != nil {
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]string{"status": "ok", "path": body.ExportPath})
|
|
}
|
|
|
|
func (s *Server) handleSkillImport(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
ImportPath string `json:"import_path"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
skill, err := skills.Import(body.ImportPath)
|
|
if err != nil {
|
|
writeError(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := skills.Create(skill); err != nil {
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{"status": "ok", "skill": skill.Name})
|
|
}
|
|
|
|
func (s *Server) handleDashboardStatus(w http.ResponseWriter, r *http.Request) {
|
|
mcpStatuses := mcp.GetAllStatuses()
|
|
lspServers := lsp.ScanServers()
|
|
skillList, _ := skills.List()
|
|
|
|
mcpHealthy := 0
|
|
mcpTotal := len(mcpStatuses)
|
|
for _, st := range mcpStatuses {
|
|
if st.Healthy {
|
|
mcpHealthy++
|
|
}
|
|
}
|
|
|
|
lspInstalled := 0
|
|
lspTotal := len(lspServers)
|
|
for _, srv := range lspServers {
|
|
if srv.Installed {
|
|
lspInstalled++
|
|
}
|
|
}
|
|
|
|
skillsDeployed := len(skillList)
|
|
var skillIssues []string
|
|
for _, sk := range skillList {
|
|
missing := skills.CheckDependencies(&sk)
|
|
if len(missing) > 0 {
|
|
for _, dep := range missing {
|
|
skillIssues = append(skillIssues, sk.Name+": missing "+dep.Type+" "+dep.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
writeJSON(w, map[string]interface{}{
|
|
"mcp": map[string]interface{}{
|
|
"total": mcpTotal,
|
|
"healthy": mcpHealthy,
|
|
"servers": mcpStatuses,
|
|
},
|
|
"lsp": map[string]interface{}{
|
|
"total": lspTotal,
|
|
"installed": lspInstalled,
|
|
"servers": lspServers,
|
|
},
|
|
"skills": map[string]interface{}{
|
|
"total": skillsDeployed,
|
|
"issues": skillIssues,
|
|
"deployed": skillList,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) {
|
|
s.scanResult = scanner.ScanSystem()
|
|
writeJSON(w, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (s *Server) handleEditors(w http.ResponseWriter, r *http.Request) {
|
|
editors := scanner.ScanEditors()
|
|
writeJSON(w, map[string]interface{}{"editors": editors})
|
|
}
|
|
|
|
func (s *Server) handleProvidersQuota(w http.ResponseWriter, r *http.Request) {
|
|
type providerQuota struct {
|
|
Name string `json:"name"`
|
|
Active bool `json:"active"`
|
|
Healthy bool `json:"healthy"`
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
var results []providerQuota
|
|
client := &http.Client{Timeout: 8 * time.Second}
|
|
for _, p := range s.config.AI.Providers {
|
|
q := providerQuota{Name: p.Name, Active: p.Active}
|
|
switch p.Name {
|
|
case "minimax":
|
|
if p.APIKey == "" {
|
|
q.Error = "no API key"
|
|
results = append(results, q)
|
|
continue
|
|
}
|
|
req, _ := http.NewRequest("GET", "https://api.minimax.io/v1/token_plan/remains", nil)
|
|
req.Header.Set("Authorization", "Bearer "+p.APIKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
q.Error = err.Error()
|
|
} else {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
var data map[string]interface{}
|
|
if json.Unmarshal(body, &data) == nil {
|
|
if models, ok := data["model_remains"].([]interface{}); ok {
|
|
filtered := make([]map[string]interface{}, 0)
|
|
for _, m := range models {
|
|
if mm, ok := m.(map[string]interface{}); ok {
|
|
usage, _ := mm["current_interval_usage_count"].(float64)
|
|
total, _ := mm["current_interval_total_count"].(float64)
|
|
if total > 0 {
|
|
filtered = append(filtered, map[string]interface{}{
|
|
"model": mm["model_name"],
|
|
"used": usage,
|
|
"total": total,
|
|
"remaining": total - usage,
|
|
"weekly_used": mm["current_weekly_usage_count"],
|
|
"weekly_total": mm["current_weekly_total_count"],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
q.Data = map[string]interface{}{"models": filtered}
|
|
q.Healthy = true
|
|
}
|
|
}
|
|
}
|
|
case "zai":
|
|
if p.APIKey == "" {
|
|
q.Error = "no API key"
|
|
results = append(results, q)
|
|
continue
|
|
}
|
|
req, _ := http.NewRequest("GET", "https://api.z.ai/api/monitor/usage/quota/limit", nil)
|
|
req.Header.Set("Authorization", "Bearer "+p.APIKey)
|
|
req.Header.Set("Accept", "application/json")
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
q.Error = err.Error()
|
|
} else {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
var data map[string]interface{}
|
|
if json.Unmarshal(body, &data) == nil {
|
|
q.Data = data
|
|
q.Healthy = true
|
|
}
|
|
}
|
|
default:
|
|
q.Error = "quota not supported"
|
|
}
|
|
results = append(results, q)
|
|
}
|
|
writeJSON(w, map[string]interface{}{"providers": results})
|
|
}
|
|
|
|
func (s *Server) handleRecentCommands(w http.ResponseWriter, r *http.Request) {
|
|
home, _ := os.UserHomeDir()
|
|
type cmdEntry struct {
|
|
Cmd string `json:"cmd"`
|
|
Shell string `json:"shell"`
|
|
}
|
|
|
|
var entries []cmdEntry
|
|
|
|
for _, histFile := range []string{".bash_history", ".zsh_history"} {
|
|
path := filepath.Join(home, histFile)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
shell := "bash"
|
|
if strings.Contains(histFile, "zsh") {
|
|
shell = "zsh"
|
|
}
|
|
lines := strings.Split(string(data), "\n")
|
|
start := len(lines) - 25
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
for i := len(lines) - 1; i >= start; i-- {
|
|
line := strings.TrimSpace(lines[i])
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, ": ") {
|
|
parts := strings.SplitN(line, ";", 2)
|
|
if len(parts) == 2 {
|
|
line = strings.TrimSpace(parts[1])
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
if line == "" {
|
|
continue
|
|
}
|
|
entries = append(entries, cmdEntry{Cmd: line, Shell: shell})
|
|
}
|
|
}
|
|
|
|
max := 20
|
|
if len(entries) > max {
|
|
entries = entries[:max]
|
|
}
|
|
|
|
writeJSON(w, map[string]interface{}{"commands": entries})
|
|
}
|
|
|
|
func (s *Server) handleRunningProcesses(w http.ResponseWriter, r *http.Request) {
|
|
type proc struct {
|
|
PID int `json:"pid"`
|
|
Name string `json:"name"`
|
|
Command string `json:"command"`
|
|
CPU string `json:"cpu"`
|
|
Mem string `json:"mem"`
|
|
}
|
|
var procs []proc
|
|
|
|
editors := []string{"code", "nvim", "vim", "emacs", "hx", "subl", "zed", "cursor"}
|
|
langs := []string{"node", "python", "java", "go", "rustc", "cargo", "ruby", "php"}
|
|
interesting := append(editors, langs...)
|
|
interesting = append(interesting, "muyue")
|
|
|
|
cmd := exec.Command("ps", "aux")
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
writeJSON(w, map[string]interface{}{"processes": procs})
|
|
return
|
|
}
|
|
|
|
lines := strings.Split(string(out), "\n")
|
|
for _, line := range lines[1:] {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 11 {
|
|
continue
|
|
}
|
|
fullCmd := strings.Join(fields[10:], " ")
|
|
name := filepath.Base(fields[10])
|
|
matched := false
|
|
for _, pattern := range interesting {
|
|
if strings.Contains(name, pattern) || strings.Contains(strings.ToLower(fullCmd), pattern) {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
if !matched {
|
|
continue
|
|
}
|
|
var pid int
|
|
fmt.Sscanf(fields[1], "%d", &pid)
|
|
procs = append(procs, proc{
|
|
PID: pid,
|
|
Name: name,
|
|
Command: fullCmd,
|
|
CPU: fields[2],
|
|
Mem: fields[3],
|
|
})
|
|
}
|
|
|
|
writeJSON(w, map[string]interface{}{"processes": procs})
|
|
}
|
|
|
|
type sysMetrics struct {
|
|
CPUPercent float64 `json:"cpu_percent"`
|
|
MemPercent float64 `json:"mem_percent"`
|
|
MemUsedMB float64 `json:"mem_used_mb"`
|
|
MemTotalMB float64 `json:"mem_total_mb"`
|
|
NetRxKBs float64 `json:"net_rx_kbs"`
|
|
NetTxKBs float64 `json:"net_tx_kbs"`
|
|
}
|
|
|
|
var (
|
|
lastCPU [2]float64
|
|
lastNet [2]float64
|
|
lastNetTs time.Time
|
|
lastCPUSet bool
|
|
)
|
|
|
|
func (s *Server) handleSystemMetrics(w http.ResponseWriter, r *http.Request) {
|
|
m := sysMetrics{}
|
|
|
|
// CPU from /proc/stat
|
|
if data, err := os.ReadFile("/proc/stat"); err == nil {
|
|
line := strings.Split(string(data), "\n")[0]
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 5 {
|
|
var idle, total float64
|
|
for i := 1; i < len(fields) && i <= 4; i++ {
|
|
var v float64
|
|
fmt.Sscanf(fields[i], "%f", &v)
|
|
total += v
|
|
if i == 4 {
|
|
idle = v
|
|
}
|
|
}
|
|
if lastCPUSet {
|
|
dIdle := idle - lastCPU[0]
|
|
dTotal := total - lastCPU[1]
|
|
if dTotal > 0 {
|
|
m.CPUPercent = (1 - dIdle/dTotal) * 100
|
|
}
|
|
}
|
|
lastCPU = [2]float64{idle, total}
|
|
lastCPUSet = true
|
|
}
|
|
}
|
|
|
|
// Memory from /proc/meminfo
|
|
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
|
var memTotal, memAvailable float64
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
var v float64
|
|
fmt.Sscanf(fields[1], "%f", &v)
|
|
switch fields[0] {
|
|
case "MemTotal:":
|
|
memTotal = v
|
|
case "MemAvailable:":
|
|
memAvailable = v
|
|
}
|
|
}
|
|
if memTotal > 0 {
|
|
m.MemTotalMB = memTotal / 1024
|
|
m.MemUsedMB = (memTotal - memAvailable) / 1024
|
|
m.MemPercent = (memTotal - memAvailable) / memTotal * 100
|
|
}
|
|
}
|
|
|
|
// Network from /proc/net/dev
|
|
if data, err := os.ReadFile("/proc/net/dev"); err == nil {
|
|
var rxBytes, txBytes float64
|
|
for _, line := range strings.Split(string(data), "\n")[2:] {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 10 {
|
|
continue
|
|
}
|
|
iface := strings.TrimSuffix(fields[0], ":")
|
|
if iface == "lo" {
|
|
continue
|
|
}
|
|
var rx, tx float64
|
|
fmt.Sscanf(fields[1], "%f", &rx)
|
|
fmt.Sscanf(fields[9], "%f", &tx)
|
|
rxBytes += rx
|
|
txBytes += tx
|
|
}
|
|
now := time.Now()
|
|
if !lastNetTs.IsZero() {
|
|
elapsed := now.Sub(lastNetTs).Seconds()
|
|
if elapsed > 0 {
|
|
m.NetRxKBs = (rxBytes - lastNet[0]) / 1024 / elapsed
|
|
m.NetTxKBs = (txBytes - lastNet[1]) / 1024 / elapsed
|
|
if m.NetRxKBs < 0 {
|
|
m.NetRxKBs = 0
|
|
}
|
|
if m.NetTxKBs < 0 {
|
|
m.NetTxKBs = 0
|
|
}
|
|
}
|
|
}
|
|
lastNet = [2]float64{rxBytes, txBytes}
|
|
lastNetTs = now
|
|
}
|
|
|
|
writeJSON(w, m)
|
|
}
|