Website: machine estimator v3 — the bar shows what the box's memory is used for (still unlisted, R-997)
gates / gates (push) Successful in 6m29s

The bar keeps its length (the fits-well box memory) and is split into base system
(box - guest), Felhom's own services, each app's peak (largest first, the dashboard's
palette minus amber and its first blue; beyond 8 apps one group) and spare room
(guest - peak) — all from the one rule; a test proves the parts sum to the bar within
1 MB. A legend under it (not sticky) and one sentence explain the recommended size.
Typical figure moved into the method fold-out. English: 'an 8 GB machine'.
This commit is contained in:
2026-10-13 16:08:00 +02:00
parent 16dca64834
commit 92e9d45cf5
10 changed files with 186 additions and 38 deletions
+38 -1
View File
@@ -59,6 +59,16 @@ def estimate(d, slugs): # mirrors sizing.js estimate()
return peak, out
def segments(d, slugs): # mirrors estimate().segments in sizing.js (v3)
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"])
peak = d["platform_mb"] + sum(a["peak_mb"] for a in apps)
well = min_box_gb(d, peak * d["headroom"])
box, guest = well * 1024, guest_mib(d, well)
return well, [("base", box - guest), ("platform", d["platform_mb"])] + \
[(a["slug"], a["peak_mb"]) for a in apps] + [("spare", guest - peak)]
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)
@@ -112,6 +122,32 @@ class SizingWeb(unittest.TestCase):
if rec is not None:
self.assertGreaterEqual(rec, gb - 1 / 1024)
def test_segments_add_up_to_the_bar(self):
for name, sel in {"household": HOUSEHOLD, "household without immich": [x for x in HOUSEHOLD if x != "immich"],
"immich alone": ["immich"], "all 62": self.all, "nothing": []}.items():
well, segs = segments(self.d, sel)
self.assertAlmostEqual(sum(mb for _, mb in segs), well * 1024, delta=1, msg=name)
self.assertGreaterEqual(dict(segs)["base"], 0, name + ": base system negative")
self.assertGreaterEqual(dict(segs)["spare"], 0, name + ": spare negative")
# the household preset, by hand: peak 5,515 MiB; the bar holds 5,515 * 1.5 = 8,272.5 in the guest
well, segs = segments(self.d, HOUSEHOLD)
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
def test_english_article(self):
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))
def say(tpl, gb): # mirrors fill() in sizing.js
art = t["an"] if str(gb) in t["an_before"] else t["a"]
return tpl.replace("{a}", art).replace("{gb}", str(gb))
got = {gb: say(t["recommend"], gb) for gb in self.d["boxes_gb"]}
self.assertEqual(got[8], "Recommended: an 8 GB machine.")
for gb in self.d["boxes_gb"]:
if gb != 8:
self.assertEqual(got[gb], "Recommended: a %d GB machine." % gb)
for k in ("tight_on", "only_tight", "none_fits"):
self.assertNotRegex(say(t[k], 8), r"\ba 8\b", k)
def test_sizing_md_table_agrees_with_the_rule(self):
with io.open(SIZING_MD, encoding="utf-8") as f:
s = f.read()
@@ -146,7 +182,8 @@ class SizingWeb(unittest.TestCase):
nums = set(re.findall(r"(?<![\w.])\d+(?:\.\d+)?", code))
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))
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"):
self.assertIn(need, js)
def test_data_has_no_peak_below_typical(self):