Added memory limits and system info for memory
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
)
|
||||
|
||||
// Router handles all /api/* requests.
|
||||
@@ -215,13 +216,8 @@ func (r *Router) getStackLogs(w http.ResponseWriter, req *http.Request, name str
|
||||
}
|
||||
|
||||
func (r *Router) systemInfo(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: map[string]interface{}{
|
||||
"customer_id": r.cfg.Customer.ID,
|
||||
"customer_name": r.cfg.Customer.Name,
|
||||
"domain": r.cfg.Customer.Domain,
|
||||
"backup_enabled": r.cfg.Backup.Enabled,
|
||||
"monitor_enabled": r.cfg.Monitoring.Enabled,
|
||||
}})
|
||||
info := system.GetInfo(r.cfg.Paths.HDDPath)
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: info})
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
@@ -44,6 +44,7 @@ type PathsConfig struct {
|
||||
DataDir string `yaml:"data_dir"`
|
||||
BackupDir string `yaml:"backup_dir"`
|
||||
DBDumpDir string `yaml:"db_dump_dir"`
|
||||
HDDPath string `yaml:"hdd_path"`
|
||||
}
|
||||
|
||||
type WebConfig struct {
|
||||
|
||||
@@ -22,6 +22,7 @@ type Metadata struct {
|
||||
// ResourceHints describe what the app needs.
|
||||
type ResourceHints struct {
|
||||
RAM string `yaml:"ram" json:"ram"`
|
||||
MemLimit string `yaml:"mem_limit" json:"mem_limit"`
|
||||
PiCompatible bool `yaml:"pi_compatible" json:"pi_compatible"`
|
||||
NeedsHDD bool `yaml:"needs_hdd" json:"needs_hdd"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package system
|
||||
|
||||
// SystemInfo holds system resource usage information.
|
||||
type SystemInfo struct {
|
||||
TotalMemMB uint64 `json:"total_mem_mb"`
|
||||
UsedMemMB uint64 `json:"used_mem_mb"`
|
||||
AvailMemMB uint64 `json:"avail_mem_mb"`
|
||||
MemPercent float64 `json:"mem_percent"`
|
||||
|
||||
DiskTotalGB float64 `json:"disk_total_gb"`
|
||||
DiskUsedGB float64 `json:"disk_used_gb"`
|
||||
DiskAvailGB float64 `json:"disk_avail_gb"`
|
||||
DiskPercent float64 `json:"disk_percent"`
|
||||
|
||||
HDDTotalGB float64 `json:"hdd_total_gb,omitempty"`
|
||||
HDDUsedGB float64 `json:"hdd_used_gb,omitempty"`
|
||||
HDDAvailGB float64 `json:"hdd_avail_gb,omitempty"`
|
||||
HDDPercent float64 `json:"hdd_percent,omitempty"`
|
||||
HDDConfigured bool `json:"hdd_configured"`
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//go:build linux
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// GetInfo reads system memory and disk usage.
|
||||
// hddPath is the mount path for external HDD; if empty, HDD info is skipped.
|
||||
func GetInfo(hddPath string) SystemInfo {
|
||||
info := SystemInfo{}
|
||||
|
||||
// --- Memory from /proc/meminfo ---
|
||||
readMemInfo(&info)
|
||||
|
||||
// --- Root filesystem disk usage ---
|
||||
readDiskUsage("/", &info.DiskTotalGB, &info.DiskUsedGB, &info.DiskAvailGB, &info.DiskPercent)
|
||||
|
||||
// --- HDD disk usage (if configured) ---
|
||||
if hddPath != "" {
|
||||
info.HDDConfigured = true
|
||||
readDiskUsage(hddPath, &info.HDDTotalGB, &info.HDDUsedGB, &info.HDDAvailGB, &info.HDDPercent)
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func readMemInfo(info *SystemInfo) {
|
||||
f, err := os.Open("/proc/meminfo")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var totalKB, availKB uint64
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
switch {
|
||||
case strings.HasPrefix(line, "MemTotal:"):
|
||||
totalKB = parseMemLine(line)
|
||||
case strings.HasPrefix(line, "MemAvailable:"):
|
||||
availKB = parseMemLine(line)
|
||||
}
|
||||
if totalKB > 0 && availKB > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if totalKB > 0 {
|
||||
info.TotalMemMB = totalKB / 1024
|
||||
info.AvailMemMB = availKB / 1024
|
||||
info.UsedMemMB = info.TotalMemMB - info.AvailMemMB
|
||||
info.MemPercent = float64(info.UsedMemMB) / float64(info.TotalMemMB) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// parseMemLine extracts the kB value from a /proc/meminfo line like "MemTotal: 16384000 kB"
|
||||
func parseMemLine(line string) uint64 {
|
||||
// Remove label prefix up to ':'
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) < 2 {
|
||||
return 0
|
||||
}
|
||||
valStr := strings.TrimSpace(parts[1])
|
||||
valStr = strings.TrimSuffix(valStr, " kB")
|
||||
valStr = strings.TrimSpace(valStr)
|
||||
|
||||
var val uint64
|
||||
for _, c := range valStr {
|
||||
if c >= '0' && c <= '9' {
|
||||
val = val*10 + uint64(c-'0')
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func readDiskUsage(path string, totalGB, usedGB, availGB *float64, percent *float64) {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bsize := uint64(stat.Bsize)
|
||||
total := stat.Blocks * bsize
|
||||
avail := stat.Bavail * bsize
|
||||
used := total - (stat.Bfree * bsize) // Bfree includes reserved blocks
|
||||
|
||||
const gb = 1024 * 1024 * 1024
|
||||
*totalGB = float64(total) / gb
|
||||
*usedGB = float64(used) / gb
|
||||
*availGB = float64(avail) / gb
|
||||
if total > 0 {
|
||||
*percent = float64(used) / float64(total) * 100
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build !linux
|
||||
|
||||
package system
|
||||
|
||||
// GetInfo returns empty system info on non-Linux platforms.
|
||||
func GetInfo(_ string) SystemInfo {
|
||||
return SystemInfo{}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -129,6 +130,34 @@ func (s *Server) loadTemplates() {
|
||||
"appPageURL": func(slug string) string {
|
||||
return s.cfg.AppPageURL(slug)
|
||||
},
|
||||
"usageColor": func(percent float64) string {
|
||||
if percent >= 85 {
|
||||
return "red"
|
||||
}
|
||||
if percent >= 70 {
|
||||
return "yellow"
|
||||
}
|
||||
return "green"
|
||||
},
|
||||
"fmtMB": func(mb uint64) string {
|
||||
if mb >= 1024 {
|
||||
gb := float64(mb) / 1024.0
|
||||
if gb >= 10 {
|
||||
return fmt.Sprintf("%.0f GB", gb)
|
||||
}
|
||||
return fmt.Sprintf("%.1f GB", gb)
|
||||
}
|
||||
return fmt.Sprintf("%d MB", mb)
|
||||
},
|
||||
"fmtGB": func(gb float64) string {
|
||||
if gb >= 100 {
|
||||
return fmt.Sprintf("%.0f GB", gb)
|
||||
}
|
||||
if gb >= 10 {
|
||||
return fmt.Sprintf("%.1f GB", gb)
|
||||
}
|
||||
return fmt.Sprintf("%.2f GB", gb)
|
||||
},
|
||||
}
|
||||
|
||||
s.tmpl = template.Must(template.New("").Funcs(funcMap).Parse(allTemplates))
|
||||
@@ -315,11 +344,14 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
sysInfo := system.GetInfo(s.cfg.Paths.HDDPath)
|
||||
|
||||
data := s.baseData("dashboard", "Vezérlőpult")
|
||||
data["Stacks"] = stackList
|
||||
data["RunningCount"] = running
|
||||
data["StoppedCount"] = stopped
|
||||
data["TotalCount"] = len(stackList)
|
||||
data["SystemInfo"] = sysInfo
|
||||
|
||||
s.render(w, "dashboard", data)
|
||||
}
|
||||
|
||||
@@ -94,6 +94,42 @@ const dashboardTmpl = `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .SystemInfo.TotalMemMB}}
|
||||
<div class="system-info-card">
|
||||
<div class="system-info-items">
|
||||
<div class="system-info-item">
|
||||
<div class="system-info-header">
|
||||
<span class="system-info-label">Memória</span>
|
||||
<span class="system-info-value">{{fmtMB .SystemInfo.UsedMemMB}} / {{fmtMB .SystemInfo.TotalMemMB}} ({{printf "%.0f" .SystemInfo.MemPercent}}%)</span>
|
||||
</div>
|
||||
<div class="system-bar">
|
||||
<div class="system-bar-fill system-bar-{{usageColor .SystemInfo.MemPercent}}" style="width:{{printf "%.0f" .SystemInfo.MemPercent}}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="system-info-item">
|
||||
<div class="system-info-header">
|
||||
<span class="system-info-label">SSD tárhely</span>
|
||||
<span class="system-info-value">{{fmtGB .SystemInfo.DiskUsedGB}} / {{fmtGB .SystemInfo.DiskTotalGB}} ({{printf "%.0f" .SystemInfo.DiskPercent}}%)</span>
|
||||
</div>
|
||||
<div class="system-bar">
|
||||
<div class="system-bar-fill system-bar-{{usageColor .SystemInfo.DiskPercent}}" style="width:{{printf "%.0f" .SystemInfo.DiskPercent}}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{if .SystemInfo.HDDConfigured}}
|
||||
<div class="system-info-item">
|
||||
<div class="system-info-header">
|
||||
<span class="system-info-label">Külső HDD</span>
|
||||
<span class="system-info-value">{{fmtGB .SystemInfo.HDDUsedGB}} / {{fmtGB .SystemInfo.HDDTotalGB}} ({{printf "%.0f" .SystemInfo.HDDPercent}}%)</span>
|
||||
</div>
|
||||
<div class="system-bar">
|
||||
<div class="system-bar-fill system-bar-{{usageColor .SystemInfo.HDDPercent}}" style="width:{{printf "%.0f" .SystemInfo.HDDPercent}}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<h3>Alkalmazások állapota</h3>
|
||||
|
||||
<div class="stack-list">
|
||||
@@ -195,9 +231,9 @@ const stacksTmpl = `
|
||||
{{if isOperational .State}}
|
||||
<button class="btn btn-success" onclick="stackAction('{{.Name}}', 'update')">Frissítés</button>
|
||||
<button class="btn btn-warning" onclick="stackAction('{{.Name}}', 'restart')">Újraindítás</button>
|
||||
<button class="btn btn-danger" onclick="stackAction('{{.Name}}', 'stop')">■ Leállítás</button>
|
||||
<button class="btn btn-danger" onclick="stackAction('{{.Name}}', 'stop')">Leállítás</button>
|
||||
{{else}}
|
||||
<button class="btn btn-success" onclick="stackAction('{{.Name}}', 'start')">▶ Indítás</button>
|
||||
<button class="btn btn-success" onclick="stackAction('{{.Name}}', 'start')">Indítás</button>
|
||||
{{end}}
|
||||
<a href="/stacks/{{.Name}}/logs" class="btn btn-outline">Naplók</a>
|
||||
<a href="{{appPageURL .Meta.Slug}}" class="btn btn-outline">Részletek</a>
|
||||
@@ -635,6 +671,57 @@ h3 {
|
||||
margin-top: .25rem;
|
||||
}
|
||||
|
||||
/* System info bar */
|
||||
.system-info-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem 1.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.system-info-items {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.system-info-item {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
.system-info-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: .4rem;
|
||||
}
|
||||
.system-info-label {
|
||||
font-size: .8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
.system-info-value {
|
||||
font-size: .8rem;
|
||||
color: var(--text-muted);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.system-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.system-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.system-bar-green { background: var(--green); }
|
||||
.system-bar-yellow { background: var(--yellow); }
|
||||
.system-bar-red { background: var(--red); }
|
||||
|
||||
/* Stack list (dashboard) */
|
||||
.stack-list {
|
||||
display: flex;
|
||||
@@ -1039,5 +1126,6 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
|
||||
.stack-grid { grid-template-columns: 1fr; }
|
||||
.stats-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
.deploy-info { flex-direction: column; }
|
||||
.system-info-items { flex-direction: column; gap: 1rem; }
|
||||
}
|
||||
`
|
||||
Reference in New Issue
Block a user