Website: machine estimator v3.1 — bar never below an 8 GB box; a single leftover app keeps its name (R-997)
gates / gates (push) Successful in 6m47s

Empty or tiny selections drew a ~2.3 GB box whose base system (1,636 MB) no real box
has; the bar is now floored at the smallest named size (8 GB) for its parts only — the
recommendation and the rule are unchanged. Grouping applies only to two or more
leftover apps; a ninth app is shown by name in the group's colour. Tests: 12, the
new ones red-proofed.
This commit is contained in:
2026-10-13 16:31:56 +02:00
parent 92e9d45cf5
commit fc0441348d
7 changed files with 73 additions and 11 deletions
File diff suppressed because one or more lines are too long
+7
View File
@@ -1,3 +1,10 @@
## 2026-10-13 (night) — estimator v3.1 tests
- `test_build_sizing_web.py` (12 tests): the bar is never below the smallest named box, its parts still sum to it
within 1 MB (nothing ticked, one tiny app, household, all 62), the base system never below the 8 GB split (4,096);
9 apps → 9 named parts, 10 apps → 8 named + a group of 2. Red-proofed: the floor removed from the JS and from the
mirror, and grouping a single app in the JS and in the mirror, each convicted.
## 2026-10-13 (evening) — estimator v3: segments and the English article under test ## 2026-10-13 (evening) — estimator v3: segments and the English article under test
- `build_sizing_web.py`: `bar_app_colours` (8) in the data file, so the script holds no number of its own. - `build_sizing_web.py`: `bar_app_colours` (8) in the data file, so the script holds no number of its own.
+36 -2
View File
@@ -63,12 +63,21 @@ def segments(d, slugs): # mirrors estimate().segments in sizi
by = {a["slug"]: a for a in d["apps"]} by = {a["slug"]: a for a in d["apps"]}
apps = sorted((by[s] for s in slugs if not by[s].get("builtin")), key=lambda a: -a["peak_mb"]) apps = sorted((by[s] for s in slugs if not by[s].get("builtin")), key=lambda a: -a["peak_mb"])
peak = d["platform_mb"] + sum(a["peak_mb"] for a in apps) peak = d["platform_mb"] + sum(a["peak_mb"] for a in apps)
well = min_box_gb(d, peak * d["headroom"]) well = max(min_box_gb(d, peak * d["headroom"]), d["boxes_gb"][0]) # v3.1: never below the smallest named box
box, guest = well * 1024, guest_mib(d, well) box, guest = well * 1024, guest_mib(d, well)
return well, [("base", box - guest), ("platform", d["platform_mb"])] + \ return well, [("base", box - guest), ("platform", d["platform_mb"])] + \
[(a["slug"], a["peak_mb"]) for a in apps] + [("spare", guest - peak)] [(a["slug"], a["peak_mb"]) for a in apps] + [("spare", guest - peak)]
def legend_parts(d, slugs): # mirrors the grouping in sizing.js draw(): named parts + the group
by = {a["slug"]: a for a in d["apps"]}
apps = sorted((by[s] for s in slugs if not by[s].get("builtin")), key=lambda a: -a["peak_mb"])
k = d["bar_app_colours"]
if len(apps) - k >= 2:
return [a["slug"] for a in apps[:k]], len(apps) - k
return [a["slug"] for a in apps], 0
def first(d, verdicts, want): # mirrors recommend_gb / tight_box_gb in sizing.js def first(d, verdicts, want): # mirrors recommend_gb / tight_box_gb in sizing.js
return next((gb for gb, v in zip(d["boxes_gb"], verdicts) if v == want), None) return next((gb for gb, v in zip(d["boxes_gb"], verdicts) if v == want), None)
@@ -134,6 +143,30 @@ class SizingWeb(unittest.TestCase):
self.assertAlmostEqual(dict(segs)["spare"], 5515 * 0.5, delta=1) self.assertAlmostEqual(dict(segs)["spare"], 5515 * 0.5, delta=1)
self.assertAlmostEqual(dict(segs)["base"], 4708, delta=1) # 4,096 for Proxmox + 612 MemTotal shortfall self.assertAlmostEqual(dict(segs)["base"], 4708, delta=1) # 4,096 for Proxmox + 612 MemTotal shortfall
def test_bar_never_below_the_smallest_named_box(self):
small = min(self.d["boxes_gb"])
base8 = small * 1024 - guest_mib(self.d, small)
tiny = ["glance"] # 6 MB: its fits-well size is far below 8 GB
self.assertLess(min_box_gb(self.d, (self.d["platform_mb"] + 7) * self.d["headroom"]), small)
for name, sel in {"nothing": [], "tiny": tiny, "household": HOUSEHOLD, "all 62": self.all}.items():
bar, segs = segments(self.d, sel)
self.assertGreaterEqual(bar, small, name)
self.assertAlmostEqual(sum(mb for _, mb in segs), bar * 1024, delta=1, msg=name)
self.assertGreaterEqual(dict(segs)["base"], base8 - 1, name + ": base below the 8 GB split")
bar, segs = segments(self.d, [])
self.assertEqual(bar, small)
self.assertEqual(dict(segs)["base"], 4096) # 8 GB: 8,192 - guest 4,096 — not the 1,636 of a 2.3 GB box
self.assertEqual(dict(segs)["spare"], 4096 - self.d["platform_mb"])
def test_grouping_keeps_a_single_leftover_named(self):
by_peak = [a["slug"] for a in sorted((a for a in self.d["apps"] if not a.get("builtin")), key=lambda a: -a["peak_mb"])]
named, grouped = legend_parts(self.d, by_peak[:9])
self.assertEqual((len(named), grouped), (9, 0)) # 9 apps -> 9 named parts, no „további 1”
named, grouped = legend_parts(self.d, by_peak[:10])
self.assertEqual((len(named), grouped), (8, 2)) # 10 apps -> 8 named + „további 2 alkalmazás”
js = io.open(JS, encoding="utf-8").read()
self.assertIn("appCount - data.bar_app_colours >= 2", js)
def test_english_article(self): def test_english_article(self):
with io.open(os.path.join(ROOT, "website", "en", "sizing.html"), encoding="utf-8-sig") as f: with io.open(os.path.join(ROOT, "website", "en", "sizing.html"), encoding="utf-8-sig") as f:
t = json.loads(re.search(r'id="sizer-strings">(.*?)</script>', f.read(), re.S).group(1)) t = json.loads(re.search(r'id="sizer-strings">(.*?)</script>', f.read(), re.S).group(1))
@@ -183,7 +216,8 @@ class SizingWeb(unittest.TestCase):
allowed = {"0", "1", "2", "100", "1024"} # 100 = percent, 1024 = MiB per GiB; no rule constant allowed = {"0", "1", "2", "100", "1024"} # 100 = percent, 1024 = MiB per GiB; no rule constant
self.assertTrue(nums <= allowed, "sizing.js holds its own numbers: %s" % sorted(nums - allowed)) self.assertTrue(nums <= allowed, "sizing.js holds its own numbers: %s" % sorted(nums - allowed))
for need in ("data.bar_max_gb", "data.bar_marks_gb", "function minBoxGb", "guestMib(data, mid) >= needMib", for need in ("data.bar_max_gb", "data.bar_marks_gb", "function minBoxGb", "guestMib(data, mid) >= needMib",
"data.bar_app_colours", "boxMb - guestMb", "guestMb - peak"): "data.bar_app_colours", "boxMb - guestMb", "guestMb - peak",
"Math.max(wellGb, data.boxes_gb[0])", "guestMib(data, barGb)"):
self.assertIn(need, js) self.assertIn(need, js)
def test_data_has_no_peak_below_typical(self): def test_data_has_no_peak_below_typical(self):
+11 -1
View File
@@ -1,4 +1,14 @@
## 2026-10-13 (evening) — machine estimator v3: the bar shows what the box's memory is used for ## 2026-10-13 (night) — machine estimator v3.1: two fixes from the reviewer's browser check
- **The bar is never drawn for a box smaller than 8 GB** (the smallest size the page names). With nothing or very
little ticked, the „fits well" size was about 2.3 GB, where the installer's ceiling clause gave a 1.6 GB base
system no real box has. Now the parts are an 8 GB box's: base system 4.0 GB, Felhom 0.5 GB, spare 3.5 GB; the
empty-selection sentence uses the same split. The recommendation and the rule are unchanged.
- **No more „további 1 alkalmazás".** The smallest apps are grouped only when two or more are left over; with exactly
nine apps the ninth keeps its name, in the group's neutral colour (no ninth palette colour).
- `sizing.js?v=4`.
## 2026-10-13 (evening) — machine estimator v3: the bar shows what the box's memory is used for
v2 said the apps use 5 GB at peak and then recommended 16 GB; the reason was invisible. Still unlisted (R-997). v2 said the apps use 5 GB at peak and then recommended 16 GB; the reason was invisible. Still unlisted (R-997).
+16 -5
View File
@@ -15,6 +15,8 @@
* v3: the bar is split into what that box memory is used for, every part from the same rule: * v3: the bar is split into what that box memory is used for, every part from the same rule:
* base system = box - guest(box) · Felhom = platform · each app = its peak · spare = guest(box) - peak * base system = box - guest(box) · Felhom = platform · each app = its peak · spare = guest(box) - peak
* so the parts add up to the box exactly (scripts/test_build_sizing_web.py proves it). * so the parts add up to the box exactly (scripts/test_build_sizing_web.py proves it).
* v3.1: the bar is never drawn for a box smaller than the smallest size the page names (boxes_gb[0]);
* below it, the installer's ceiling clause would show a base system no real box has.
* *
* The page text is not here: each page carries its own strings (#sizer-strings), so this file is the * The page text is not here: each page carries its own strings (#sizer-strings), so this file is the
* same for both languages. * same for both languages.
@@ -68,7 +70,8 @@
}); });
var heavy = busy.filter(function (a) { return a.cpu_class === 'heavy-in-bursts'; }); var heavy = busy.filter(function (a) { return a.cpu_class === 'heavy-in-bursts'; });
var wellGb = minBoxGb(data, peak * data.headroom); var wellGb = minBoxGb(data, peak * data.headroom);
var boxMb = wellGb * 1024, guestMb = guestMib(data, wellGb); var barGb = Math.max(wellGb, data.boxes_gb[0]);
var boxMb = barGb * 1024, guestMb = guestMib(data, barGb);
var apps = slugs.map(function (s) { return bySlug[s]; }) var apps = slugs.map(function (s) { return bySlug[s]; })
.filter(function (a) { return a && !a.builtin; }) .filter(function (a) { return a && !a.builtin; })
.sort(function (x, y) { return y.peak_mb - x.peak_mb; }); .sort(function (x, y) { return y.peak_mb - x.peak_mb; });
@@ -77,7 +80,7 @@
segments.push({ kind: 'spare', mb: guestMb - peak }); segments.push({ kind: 'spare', mb: guestMb - peak });
return { return {
typical_mb: typical, peak_mb: peak, boxes: boxes, busy: busy, heavy: heavy, unmeasured: unmeasured, typical_mb: typical, peak_mb: peak, boxes: boxes, busy: busy, heavy: heavy, unmeasured: unmeasured,
well_gb: wellGb, tight_gb: minBoxGb(data, peak), well_gb: wellGb, bar_gb: barGb, tight_gb: minBoxGb(data, peak),
recommend_gb: recommend, tight_box_gb: tightBox, recommend_gb: recommend, tight_box_gb: tightBox,
segments: segments, base_mb: boxMb - guestMb, apps_peak_mb: peak - data.platform_mb segments: segments, base_mb: boxMb - guestMb, apps_peak_mb: peak - data.platform_mb
}; };
@@ -181,13 +184,21 @@
// the bar: its length is the "fits well" box memory, split into what that memory is used for // the bar: its length is the "fits well" box memory, split into what that memory is used for
// (estimate().segments); beyond the colour count the smallest apps join one segment // (estimate().segments); beyond the colour count the smallest apps join one segment
var basis = Math.max(r.well_gb, max) * 1024; var basis = Math.max(r.bar_gb, max) * 1024;
var segs = [], rest = { kind: 'more', mb: 0, n: 0 }, nApps = 0; var segs = [], rest = { kind: 'more', mb: 0, n: 0 }, nApps = 0;
var appCount = r.segments.filter(function (sg) { return sg.kind === 'app'; }).length;
// group the smallest only when TWO or more are left over; a single leftover keeps its name
// (in the group's colour — no ninth palette colour)
var group = appCount - data.bar_app_colours >= 2;
r.segments.forEach(function (sg) { r.segments.forEach(function (sg) {
if (sg.kind === 'app') { if (sg.kind === 'app') {
nApps += 1; nApps += 1;
if (nApps > data.bar_app_colours) { rest.mb += sg.mb; rest.n += 1; return; } if (nApps > data.bar_app_colours) {
sg.colour = 'sizer-c' + nApps; if (group) { rest.mb += sg.mb; rest.n += 1; return; }
sg.colour = 'sizer-seg-more';
} else {
sg.colour = 'sizer-c' + nApps;
}
} }
if (sg.kind === 'spare' && rest.n) { segs.push(rest); } if (sg.kind === 'spare' && rest.n) { segs.push(rest); }
segs.push(sg); segs.push(sg);
+1 -1
View File
@@ -19,7 +19,7 @@
<link rel="icon" type="image/png" href="/assets/logo_favicon_2.svg"> <link rel="icon" type="image/png" href="/assets/logo_favicon_2.svg">
<script defer src="https://stats.felhom.eu/script.js" data-website-id="d419db57-5e1c-48df-bdb3-c29f25fb1661"></script> <script defer src="https://stats.felhom.eu/script.js" data-website-id="d419db57-5e1c-48df-bdb3-c29f25fb1661"></script>
<link rel="stylesheet" href="/assets/sizing.css?v=3"> <link rel="stylesheet" href="/assets/sizing.css?v=3">
<script defer src="/assets/sizing.js?v=3"></script> <script defer src="/assets/sizing.js?v=4"></script>
</head> </head>
<body class="shell page-gepigeny"> <body class="shell page-gepigeny">
<header class="mobile-topbar"> <header class="mobile-topbar">
+1 -1
View File
@@ -19,7 +19,7 @@
<link rel="icon" type="image/png" href="/assets/logo_favicon_2.svg"> <link rel="icon" type="image/png" href="/assets/logo_favicon_2.svg">
<script defer src="https://stats.felhom.eu/script.js" data-website-id="d419db57-5e1c-48df-bdb3-c29f25fb1661"></script> <script defer src="https://stats.felhom.eu/script.js" data-website-id="d419db57-5e1c-48df-bdb3-c29f25fb1661"></script>
<link rel="stylesheet" href="/assets/sizing.css?v=3"> <link rel="stylesheet" href="/assets/sizing.css?v=3">
<script defer src="/assets/sizing.js?v=3"></script> <script defer src="/assets/sizing.js?v=4"></script>
</head> </head>
<body class="shell page-gepigeny"> <body class="shell page-gepigeny">
<header class="mobile-topbar"> <header class="mobile-topbar">