feat: add storage info to settings page and improve NFS export handling

Backend:
- Add source/used_by/available/error fields to diskUsage in system status
- collectDiskUsage() now reads /proc/mounts, samba shares and NFS exports from DB
- Paths are deduplicated; shared paths list all shares/exports using them
- syscall.Statfs errors surface as available=false with user-facing error
- collectServiceStatus made a method of Server (receiver consistency)

Frontend:
- Settings page now shows two cards: mount points and shared resources
- Each path shows source badge (Sistema/Mount/SMB/NFS), used_by chips, progress bar
- Unavailable paths show amber warning instead of progress bar
- DiskUsage interface updated with new fields
- NFSExport interface updated with structured fields (fsid, async, etc)
- NFS page updated to use new export fields
This commit is contained in:
2026-07-05 22:56:39 -04:00
parent 72a8336087
commit 65cd753818
14 changed files with 696 additions and 100 deletions
+95 -30
View File
@@ -1,7 +1,10 @@
package web
import (
"bufio"
"net/http"
"os"
"sort"
"strings"
"syscall"
@@ -9,11 +12,15 @@ import (
)
type diskUsage struct {
Path string `json:"path"`
TotalBytes uint64 `json:"total_bytes"`
FreeBytes uint64 `json:"free_bytes"`
UsedBytes uint64 `json:"used_bytes"`
UsedPercent float64 `json:"used_percent"`
Path string `json:"path"`
TotalBytes uint64 `json:"total_bytes"`
FreeBytes uint64 `json:"free_bytes"`
UsedBytes uint64 `json:"used_bytes"`
UsedPercent float64 `json:"used_percent"`
Source string `json:"source"`
UsedBy []string `json:"used_by,omitempty"`
Available bool `json:"available"`
Error string `json:"error,omitempty"`
}
type serviceStatus struct {
@@ -24,39 +31,97 @@ type serviceStatus struct {
func (s *Server) handleSystemStatus(w http.ResponseWriter, r *http.Request) {
status := map[string]any{
"disks": collectDiskUsage(),
"services": collectServiceStatus(r),
"disks": s.collectDiskUsage(),
"services": s.collectServiceStatus(r),
}
writeJSON(w, http.StatusOK, status)
}
func collectDiskUsage() []diskUsage {
paths := []string{"/"}
var usages []diskUsage
for _, path := range paths {
var stat syscall.Statfs_t
if err := syscall.Statfs(path, &stat); err != nil {
continue
func (s *Server) collectDiskUsage() []diskUsage {
entries := make(map[string]*diskUsage)
entries["/"] = &diskUsage{Path: "/", Source: "system", Available: true}
shares, err := s.DB.ListSambaShares()
if err == nil {
for _, share := range shares {
key := share.Path
if e, ok := entries[key]; ok {
e.UsedBy = append(e.UsedBy, share.Name)
} else {
entries[key] = &diskUsage{Path: key, Source: "samba", UsedBy: []string{share.Name}, Available: true}
}
}
total := stat.Blocks * uint64(stat.Bsize)
free := stat.Bavail * uint64(stat.Bsize)
used := total - free
var pct float64
if total > 0 {
pct = float64(used) / float64(total) * 100
}
usages = append(usages, diskUsage{
Path: path,
TotalBytes: total,
FreeBytes: free,
UsedBytes: used,
UsedPercent: pct,
})
}
return usages
exports, err := s.DB.ListNFSExports()
if err == nil {
for _, exp := range exports {
key := exp.Path
label := "nfs:" + exp.Path
if e, ok := entries[key]; ok {
e.UsedBy = append(e.UsedBy, label)
} else {
entries[key] = &diskUsage{Path: key, Source: "nfs", UsedBy: []string{label}, Available: true}
}
}
}
readProcMounts(entries)
var result []diskUsage
for _, e := range entries {
fillDiskUsage(e)
result = append(result, *e)
}
sort.Slice(result, func(i, j int) bool {
if result[i].Source != result[j].Source {
return result[i].Source < result[j].Source
}
return result[i].Path < result[j].Path
})
return result
}
func collectServiceStatus(r *http.Request) []serviceStatus {
func readProcMounts(entries map[string]*diskUsage) {
f, err := os.Open("/proc/mounts")
if err != nil {
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 3 {
continue
}
mountPoint := fields[1]
if _, ok := entries[mountPoint]; !ok {
entries[mountPoint] = &diskUsage{Path: mountPoint, Source: "mount", Available: true}
}
}
}
func fillDiskUsage(e *diskUsage) {
var stat syscall.Statfs_t
if err := syscall.Statfs(e.Path, &stat); err != nil {
e.Available = false
e.Error = "path no disponible"
return
}
e.Available = true
e.TotalBytes = stat.Blocks * uint64(stat.Bsize)
e.FreeBytes = stat.Bavail * uint64(stat.Bsize)
e.UsedBytes = e.TotalBytes - e.FreeBytes
if e.TotalBytes > 0 {
e.UsedPercent = float64(e.UsedBytes) / float64(e.TotalBytes) * 100
}
}
func (s *Server) collectServiceStatus(r *http.Request) []serviceStatus {
services := []string{"smbd", "nfs-server"}
var statuses []serviceStatus
for _, name := range services {