R-889: every disk percent is df's (used / (used + available)); the tile says Rendszer, not Rendszer (/) (09 decision 138)

The box divided used by the whole filesystem, so the 5 % reserved for root made a full disk read ~5 points low
(61.8 of 68.7 GB = 90 % where df said 95 %) and the fill alarms fire late. One function, DFUsedPercent, now serves
GetDiskUsage, readDiskUsage, the recovery-unit headroom projection and the deploy page's free percent. Tests use
numbers measured on demo-hp 9201 (/mnt/sys_drive: old 21.5 %, df 23 %); a source scan refuses a percent divided by
the whole filesystem (red-proved by putting the old line back). The label measured the docker data volume, not /.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-06 08:03:21 +02:00
parent c67b26be28
commit bee2c2d815
15 changed files with 101 additions and 13 deletions
+1 -1
View File
@@ -416,7 +416,7 @@ func (m *Manager) floorVerdict(u *UnitSpace, estGiB float64) (*UnitSpace, floorR
availAfter := u.AvailGB - estGiB availAfter := u.AvailGB - estGiB
usedAfter := u.UsedPercent usedAfter := u.UsedPercent
if u.TotalGB > 0 { if u.TotalGB > 0 {
usedAfter = (u.UsedGB + estGiB) / u.TotalGB * 100 usedAfter = system.DFUsedPercent(u.UsedGB+estGiB, u.AvailGB-estGiB) // R-889: df's number after the write
} }
if availAfter < FloorFreeGiB || usedAfter >= FloorUsedPercent { if availAfter < FloorFreeGiB || usedAfter >= FloorUsedPercent {
return u, floorSize return u, floorSize
+1 -1
View File
@@ -630,7 +630,7 @@
"common.magyar_felulet": "Hungarian interface", "common.magyar_felulet": "Hungarian interface",
"common.megsem": "Cancel", "common.megsem": "Cancel",
"common.pi_kompatibilis": "Runs on Pi", "common.pi_kompatibilis": "Runs on Pi",
"common.rendszer_gyoker": "System (/)", "common.rendszer_gyoker": "System",
"common.torles": "Delete", "common.torles": "Delete",
"common.ugyfel_azonosito": "Customer ID", "common.ugyfel_azonosito": "Customer ID",
"common.ujrainditas": "Restart", "common.ujrainditas": "Restart",
+1 -1
View File
@@ -625,7 +625,7 @@
"common.magyar_felulet": "Magyar felület", "common.magyar_felulet": "Magyar felület",
"common.megsem": "Mégsem", "common.megsem": "Mégsem",
"common.pi_kompatibilis": "Pi kompatibilis", "common.pi_kompatibilis": "Pi kompatibilis",
"common.rendszer_gyoker": "Rendszer (/)", "common.rendszer_gyoker": "Rendszer",
"common.torles": "Törlés", "common.torles": "Törlés",
"common.ugyfel_azonosito": "Ügyfél azonosító", "common.ugyfel_azonosito": "Ügyfél azonosító",
"common.ujrainditas": "Újraindítás", "common.ujrainditas": "Újraindítás",
+20
View File
@@ -0,0 +1,20 @@
package system
// DFUsedPercent is the used percentage the way `df` prints it: used / (used + available). The blocks reserved for root
// (5 % on ext4 by default) are in neither term, so a full disk reads 100 % here. Every disk percent the box shows or
// alarms on is computed by this one function (R-889, operator ruling 2026-10-06 — `09` §3 decision 138): until then
// the box divided by the whole filesystem and read ~5 points LOW on a full disk (61.8 of 68.7 GB = 90 % where `df`
// said 95 %). `df` rounds up; this does not, so the two agree within one point. Units only need to match.
// Pinned by TestDFUsedPercent_MatchesDFOnRealNumbers and TestR889_EveryDiskPercentIsTheDFOne.
func DFUsedPercent(used, avail float64) float64 {
if used <= 0 && avail <= 0 {
return 0
}
if used < 0 {
used = 0
}
if avail < 0 {
avail = 0
}
return used / (used + avail) * 100
}
@@ -0,0 +1,68 @@
package system
import (
"math"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
// R-889 — real numbers, measured 2026-10-06 07:55 on demo-hp guest 9201 (`df -P` + `stat -f`):
//
// /mnt/sys_drive df: 72064432 1K-blocks, used 15460836, avail 52917208, Use% 23
// stat -f: bsize=4096 blocks=18016108 free=14150899 avail=13229302
//
// The old formula ((blocks-free)/blocks) gave 21.5 % there; df gives 23 %. And the R-516 (11) shape: 61.8 of 68.7 GB
// with 5 % reserved read 90 % where df read 95 %.
func TestDFUsedPercent_MatchesDFOnRealNumbers(t *testing.T) {
const bs = 4096.0
blocks, free, avail := 18016108.0, 14150899.0, 13229302.0
used := (blocks - free) * bs
got := DFUsedPercent(used, avail*bs)
if math.Ceil(got) != 23 {
t.Fatalf("demo-hp /mnt/sys_drive: DFUsedPercent = %.2f, df prints 23%% (it rounds up)", got)
}
old := (blocks - free) / blocks * 100
if math.Abs(got-old) < 1 {
t.Fatalf("the df number (%.2f) must differ from the old used/total (%.2f) on a disk with reserved blocks", got, old)
}
// R-516 (11): a 68.7 GB filesystem with 5 % reserved, 61.8 GB used.
total, usedGB := 68.7, 61.8
availGB := total*0.95 - usedGB
if p := DFUsedPercent(usedGB, availGB); math.Round(p) != 95 {
t.Fatalf("the R-516 shape reads %.1f %%, df read 95 %%", p)
}
if DFUsedPercent(0, 0) != 0 || DFUsedPercent(10, 0) != 100 {
t.Fatal("empty → 0, nothing available → 100")
}
}
// Every disk percent in the controller is the df one: no production Go file divides a used/avail figure by the whole
// filesystem any more. Red-proof: put `float64(used) / float64(total) * 100` back in mounts_linux.go — this fails.
func TestR889_EveryDiskPercentIsTheDFOne(t *testing.T) {
bad := regexp.MustCompile(`(?:/\s*float64\(total\)\s*\*\s*100|/\s*[A-Za-z_.]*TotalGB\s*\*\s*100)`)
root := filepath.Join("..", "..")
var hits []string
for _, dir := range []string{"internal", "cmd"} {
_ = filepath.Walk(filepath.Join(root, dir), func(p string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") {
return nil
}
b, rerr := os.ReadFile(p)
if rerr != nil {
return nil
}
for i, line := range strings.Split(string(b), "\n") {
if bad.MatchString(line) && !strings.HasPrefix(strings.TrimSpace(line), "//") {
hits = append(hits, p+":"+itoa(uint64(i+1))+": "+strings.TrimSpace(line))
}
}
return nil
})
}
if len(hits) > 0 {
t.Fatalf("a disk percent divides by the whole filesystem (not df's used/(used+avail)) — use DFUsedPercent:\n%s", strings.Join(hits, "\n"))
}
}
+1 -1
View File
@@ -279,7 +279,7 @@ func readDiskUsage(path string, totalGB, usedGB, availGB *float64, percent *floa
*usedGB = float64(used) / gb *usedGB = float64(used) / gb
*availGB = float64(avail) / gb *availGB = float64(avail) / gb
if total > 0 { if total > 0 {
*percent = float64(used) / float64(total) * 100 *percent = DFUsedPercent(float64(used), float64(avail)) // R-889: df's number
} }
debugf("[DEBUG] [system] readDiskUsage: path=%q bsize=%d total=%.1fGB used=%.1fGB avail=%.1fGB (%.1f%%)", debugf("[DEBUG] [system] readDiskUsage: path=%q bsize=%d total=%.1fGB used=%.1fGB avail=%.1fGB (%.1f%%)",
path, bsize, *totalGB, *usedGB, *availGB, *percent) path, bsize, *totalGB, *usedGB, *availGB, *percent)
+1 -1
View File
@@ -82,7 +82,7 @@ func GetDiskUsage(path string) *DiskUsageInfo {
AvailGB: float64(avail) / float64(gb), AvailGB: float64(avail) / float64(gb),
} }
if total > 0 { if total > 0 {
info.UsedPercent = float64(used) / float64(total) * 100 info.UsedPercent = DFUsedPercent(float64(used), float64(avail)) // R-889: df's number
} }
info.TotalHuman = formatGB(info.TotalGB) info.TotalHuman = formatGB(info.TotalGB)
info.UsedHuman = formatGB(info.UsedGB) info.UsedHuman = formatGB(info.UsedGB)
+1 -1
View File
@@ -569,7 +569,7 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
if di := system.GetDiskUsage(sp.Path); di != nil { if di := system.GetDiskUsage(sp.Path); di != nil {
dp.FreeHuman = formatFreeSpace(di.AvailGB) dp.FreeHuman = formatFreeSpace(di.AvailGB)
if di.TotalGB > 0 { if di.TotalGB > 0 {
dp.FreePercent = di.AvailGB / di.TotalGB * 100 dp.FreePercent = 100 - system.DFUsedPercent(di.UsedGB, di.AvailGB) // R-889: df's number
} }
} }
deployPaths = append(deployPaths, dp) deployPaths = append(deployPaths, dp)
@@ -223,7 +223,7 @@
<div class="storage-item meter nominal"> <div class="storage-item meter nominal">
<div class="storage-header"> <div class="storage-header">
<span class="storage-label">Rendszer (/)</span> <span class="storage-label">Rendszer</span>
<span class="storage-value">42.5 GB / 100 GB (42%)</span> <span class="storage-value">42.5 GB / 100 GB (42%)</span>
</div> </div>
<div class="meter-track"> <div class="meter-track">
@@ -224,7 +224,7 @@
<div class="system-info-item meter crit"> <div class="system-info-item meter crit">
<div class="system-info-header"> <div class="system-info-header">
<span class="system-info-label">Rendszer (/)</span> <span class="system-info-label">Rendszer</span>
<span class="system-info-value">97.0 GB / 100 GB (97%)</span> <span class="system-info-value">97.0 GB / 100 GB (97%)</span>
</div> </div>
<div class="meter-track"> <div class="meter-track">
@@ -224,7 +224,7 @@
<div class="system-info-item meter warn"> <div class="system-info-item meter warn">
<div class="system-info-header"> <div class="system-info-header">
<span class="system-info-label">Rendszer (/)</span> <span class="system-info-label">Rendszer</span>
<span class="system-info-value">80.0 GB / 100 GB (80%)</span> <span class="system-info-value">80.0 GB / 100 GB (80%)</span>
</div> </div>
<div class="meter-track"> <div class="meter-track">
@@ -216,7 +216,7 @@
<div class="system-info-item meter meter-empty"> <div class="system-info-item meter meter-empty">
<div class="system-info-header"> <div class="system-info-header">
<span class="system-info-label">Rendszer (/)</span> <span class="system-info-label">Rendszer</span>
<span class="system-info-value">— <span class="state-text-neutral">nem ismert</span></span> <span class="system-info-value">— <span class="state-text-neutral">nem ismert</span></span>
</div> </div>
<div class="meter-track"></div> <div class="meter-track"></div>
@@ -246,7 +246,7 @@
<div class="storage-item meter nominal"> <div class="storage-item meter nominal">
<div class="storage-header"> <div class="storage-header">
<span class="storage-label">Rendszer (/)</span> <span class="storage-label">Rendszer</span>
<span class="storage-value">40.0 GB / 100 GB (40%)</span> <span class="storage-value">40.0 GB / 100 GB (40%)</span>
</div> </div>
<div class="meter-track"> <div class="meter-track">
@@ -246,7 +246,7 @@
<div class="storage-item meter nominal"> <div class="storage-item meter nominal">
<div class="storage-header"> <div class="storage-header">
<span class="storage-label">Rendszer (/)</span> <span class="storage-label">Rendszer</span>
<span class="storage-value">40.0 GB / 100 GB (40%)</span> <span class="storage-value">40.0 GB / 100 GB (40%)</span>
</div> </div>
<div class="meter-track"> <div class="meter-track">
@@ -246,7 +246,7 @@
<div class="storage-item meter nominal"> <div class="storage-item meter nominal">
<div class="storage-header"> <div class="storage-header">
<span class="storage-label">Rendszer (/)</span> <span class="storage-label">Rendszer</span>
<span class="storage-value">40.0 GB / 100 GB (40%)</span> <span class="storage-value">40.0 GB / 100 GB (40%)</span>
</div> </div>
<div class="meter-track"> <div class="meter-track">