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
+9
View File
@@ -1,3 +1,12 @@
## 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.
- `test_build_sizing_web.py` (10 tests): the bar's segments (base system = box − guest, Felhom, each app's peak,
spare = guest − peak) add up to the bar within 1 MB for the four cases and for nothing selected; base and spare are
never negative; the household preset by hand (spare = 5,515 × 0.5, base = 4,096 + 612); every English
recommendation gets „an 8" / „a 12" … Red-proofed: the „an 8" rule removed, the JS computing spare from the typical
figure, and segments off by 50 MB each convicted.
## 2026-10-13 (afternoon) — estimator v2: `build_sizing_web.py` box sizes, tests for the inverse
- `build_sizing_web.py`: `boxes_gb` = 8, 12, 16, 24, 32, 48, 64; new `bar_marks_gb` and `bar_max_gb`.
+2
View File
@@ -36,6 +36,7 @@ GUEST_RULE = {"reserve_mib": 4096, "max_gap_mib": 2048, "min_mib": 4096, "ceilin
BOXES_GB = [8, 12, 16, 24, 32, 48, 64] # the common sizes a recommendation names (v2: 12, 24, 48, 64 added)
BAR_MARKS_GB = [8, 16, 24, 32, 48, 64] # the memory bar's marks; the last one reads "64+"
BAR_MAX_GB = 64 # the bar's linear scale is 0..BAR_MAX_GB
BAR_APP_COLOURS = 8 # v3: apps get their own colour up to this many; the rest join one segment
BUILTIN = {"filebrowser"}
# One short line per medium / heavy-in-bursts app (the 9 of 2026-10-13). sizing.json's English notes, tidied.
@@ -119,6 +120,7 @@ def build():
boxes_gb=BOXES_GB,
bar_marks_gb=BAR_MARKS_GB,
bar_max_gb=BAR_MAX_GB,
bar_app_colours=BAR_APP_COLOURS,
categories=[dict(id=h[0], id_en=e[0], name_hu=h[1], name_en=e[1]) for h, e in zip(hu_cats, en_cats)],
apps=apps,
)
+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):