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
+57 -7
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"embed"
"encoding/json"
"fmt"
"os"
"path/filepath"
@@ -86,13 +87,62 @@ func (m *Module) Apply(ctx context.Context, database *db.DB) error {
return database.ClearDirty(ModuleName)
}
// clientSpec builds the "client(opts) client(opts)" segment of an exports line.
// If no clients are configured, it defaults to "*(opts)".
func clientSpec(clients []string, options string) string {
opts := strings.TrimSpace(options)
if opts == "" {
opts = "ro"
func buildFlags(e db.NFSExport) string {
parts := make([]string, 0, 8)
if e.ReadOnly {
parts = append(parts, "ro")
} else {
parts = append(parts, "rw")
}
if e.Async {
parts = append(parts, "async")
} else {
parts = append(parts, "sync")
}
if e.SubtreeCheck {
parts = append(parts, "subtree_check")
} else {
parts = append(parts, "no_subtree_check")
}
if e.RootSquash {
parts = append(parts, "root_squash")
} else {
parts = append(parts, "no_root_squash")
}
if e.Advanced != "" && e.Advanced != "{}" {
var adv db.NFSAdvanced
if err := json.Unmarshal([]byte(e.Advanced), &adv); err == nil {
if adv.AllSquash {
parts = append(parts, "all_squash")
} else {
parts = append(parts, "no_all_squash")
}
if adv.Secure {
parts = append(parts, "secure")
} else {
parts = append(parts, "insecure")
}
if adv.WDelay {
parts = append(parts, "wdelay")
} else {
parts = append(parts, "no_wdelay")
}
if adv.Hide {
parts = append(parts, "hide")
} else {
parts = append(parts, "nohide")
}
if adv.Crossmnt {
parts = append(parts, "crossmnt")
}
}
}
parts = append(parts, fmt.Sprintf("fsid=%d", e.FSID))
return strings.Join(parts, ",")
}
func clientSpec(clients []string, e db.NFSExport) string {
opts := buildFlags(e)
if len(clients) == 0 {
return fmt.Sprintf("*(%s)", opts)
}
@@ -125,7 +175,7 @@ func (m *Module) renderConfig(exports []db.NFSExport) ([]byte, error) {
for _, export := range exports {
data.Exports = append(data.Exports, templateExport{
Path: export.Path,
ClientSpec: clientSpec(export.Clients, export.Options),
ClientSpec: clientSpec(export.Clients, export),
})
}