Website: the machine estimator (/gepigeny, /en/sizing), live but unlisted; SIZING.md §5 in three verdicts; R-997 filed
gates / gates (push) Successful in 6m42s
gates / gates (push) Successful in 6m42s
A household ticks apps and sees, for an 8, 16 and 32 GB box: fits well / fits, but tight / does not fit, the totals, and the apps that burst on CPU. One rule in assets/sizing.js, every number from assets/sizing-data.json (built by scripts/build_sizing_web.py from the 2026-10-13 N100 measurement; test: 7 cases, red-proofed). Unlisted: noindex, robots Disallow, not in the sitemap, linked from nowhere — site_gates.py gate 25 enforces it (UNLISTED twin pairs keep every twin check). SIZING.md §5 corrected: the 8 GB line contradicted its own x1.5 rule; guest sizes 4.0/11.4/27.4 GB with the measured 612 MiB MemTotal shortfall. R-993 gains the fact that the installer accepts a host below any minimum.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""build_sizing_web.py — website/assets/sizing-data.json for the machine estimator page.
|
||||
|
||||
python3 -I scripts/build_sizing_web.py write the file
|
||||
python3 -I scripts/build_sizing_web.py --check exit 1 if the committed file differs from a fresh build
|
||||
|
||||
Inputs (read only):
|
||||
documentation/audits/sizing-2026-10-13/sizing.json the 2026-10-13 N100 measurement
|
||||
website/alkalmazasok.html, website/en/apps.html the apps the site lists, their names and categories
|
||||
|
||||
The page offers EXACTLY the apps the two apps pages list. Refusals (exit 1, nothing written):
|
||||
- an app listed on a page but absent from sizing.json (FileBrowser excepted, BY NAME: it is built
|
||||
into every box and its memory is inside the platform figure — SIZING.md §1)
|
||||
- an app whose peak is below its typical figure
|
||||
- the two pages listing different apps
|
||||
The rule's numbers live HERE and in the data file only; website/assets/sizing.js reads them from the
|
||||
file and has none of its own (scripts/test_build_sizing_web.py checks that).
|
||||
"""
|
||||
import hashlib, io, json, os, re, sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SIZING = os.path.join(ROOT, "documentation", "audits", "sizing-2026-10-13", "sizing.json")
|
||||
SOURCE_COMMIT = "c885b5ac" # the commit that added sizing.json; the sha256 below shows when it moved on
|
||||
HU_PAGE = os.path.join(ROOT, "website", "alkalmazasok.html")
|
||||
EN_PAGE = os.path.join(ROOT, "website", "en", "apps.html")
|
||||
OUT = os.path.join(ROOT, "website", "assets", "sizing-data.json")
|
||||
|
||||
# SIZING.md §1/§5: platform inside the guest = its containers (255 MB) + the Docker daemons (≈ 220 MB RSS)
|
||||
PLATFORM_MB = 470
|
||||
HEADROOM = 1.5
|
||||
# scripts/felhom-host-install.sh autosize_guest_caps(): guest = clamp(host-4096, min 4096, max host-2048),
|
||||
# then the ceiling host-1024. `host` is MemTotal, which is below the label: the N100 sold as 16 GB reports
|
||||
# 15,772 MiB (measured 2026-10-13), so a box of N GB is taken as N*1024 - 612 MiB.
|
||||
GUEST_RULE = {"reserve_mib": 4096, "max_gap_mib": 2048, "min_mib": 4096, "ceiling_gap_mib": 1024,
|
||||
"memtotal_shortfall_mib": 612}
|
||||
BOXES_GB = [8, 16, 32]
|
||||
BUILTIN = {"filebrowser"}
|
||||
|
||||
# One short line per medium / heavy-in-bursts app (the 9 of 2026-10-13). sizing.json's English notes, tidied.
|
||||
CPU_NOTES = {
|
||||
"immich": ("csak amíg új fotókat dolgoz fel (bélyegképek, arcok, keresés): 200 fotó kb. 6 perc",
|
||||
"only while it processes new photos (thumbnails, faces, search): 200 photos took about 6 minutes"),
|
||||
"jellyfin": ("csak amíg egy filmet átalakít egy olyan eszköznek, amely azt nem tudja lejátszani",
|
||||
"only while it converts a film for a device that cannot play it"),
|
||||
"paperless-ngx": ("csak amíg új beszkennelt dokumentumokat olvas be (szövegfelismerés, kb. 4 oldal percenként)",
|
||||
"only while it reads new scanned documents (text recognition, about 4 pages a minute)"),
|
||||
"calibre-web": ("csak amíg új könyveket vesz fel", "only while it imports new books"),
|
||||
"romm": ("az első indításkor; a játékkönyvtár átnézését nem mértük", "at first start; a library scan was not measured"),
|
||||
"onlyoffice": ("csak az első indításkor", "only at first start"),
|
||||
"grimmory": ("csak az első indításkor", "only at first start"),
|
||||
"tandoor": ("csak az első indításkor", "only at first start"),
|
||||
"adventurelog": ("csak az első indításkor", "only at first start"),
|
||||
}
|
||||
|
||||
|
||||
def fail(msg):
|
||||
print("build_sizing_web: REFUSED —", msg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def text(s):
|
||||
return re.sub(r"\s+", " ", re.sub(r"<[^>]+>", "", s)).replace("&", "&").strip()
|
||||
|
||||
|
||||
def parse_apps_page(path):
|
||||
t = io.open(path, encoding="utf-8-sig").read()
|
||||
cats, apps = [], []
|
||||
for sec in re.finditer(r'<div class="category-section" id="([^"]+)">(.*?)(?=<div class="category-section"|<section class="stats-section")', t, re.S):
|
||||
cats.append((sec.group(1), text(re.search(r"<h2>(.*?)</h2>", sec.group(2), re.S).group(1))))
|
||||
for c in re.finditer(r'<div class="app-card" id="([^"]+)">.*?<h3>(.*?)</h3>(?:<span class="app-tagline">(.*?)</span>)?', sec.group(2), re.S):
|
||||
apps.append(dict(slug=c.group(1), category=len(cats) - 1, name=text(c.group(2)), tagline=text(c.group(3) or "")))
|
||||
return cats, apps
|
||||
|
||||
|
||||
def build():
|
||||
raw = open(SIZING, "rb").read()
|
||||
sz = json.loads(raw)
|
||||
S = {a["slug"]: a for a in sz["apps"]}
|
||||
hu_cats, hu = parse_apps_page(HU_PAGE)
|
||||
en_cats, en = parse_apps_page(EN_PAGE)
|
||||
if [a["slug"] for a in hu] != [a["slug"] for a in en] or len(hu_cats) != len(en_cats):
|
||||
fail("the Hungarian and English apps pages do not list the same apps in the same order")
|
||||
E = {a["slug"]: a for a in en}
|
||||
apps = []
|
||||
for a in hu:
|
||||
slug = a["slug"]
|
||||
rec = dict(slug=slug, name_hu=a["name"], name_en=E[slug]["name"], tagline_hu=a["tagline"],
|
||||
tagline_en=E[slug]["tagline"], category=hu_cats[a["category"]][0])
|
||||
if slug in BUILTIN:
|
||||
rec.update(builtin=True, typical_mb=0, peak_mb=0, cpu_class="light", cpu_note_hu="", cpu_note_en="",
|
||||
measured=True)
|
||||
else:
|
||||
if slug not in S:
|
||||
fail("%s is listed on the apps pages but missing from sizing.json" % slug)
|
||||
s = S[slug]
|
||||
if s["mem_peak_mb"] is None or s["mem_typical_mb"] is None:
|
||||
fail("%s has no memory figure in sizing.json" % slug)
|
||||
if s["mem_peak_mb"] < s["mem_typical_mb"]:
|
||||
fail("%s: peak %s < typical %s in sizing.json" % (slug, s["mem_peak_mb"], s["mem_typical_mb"]))
|
||||
cls = s["cpu_class"]
|
||||
note = CPU_NOTES.get(slug, ("", ""))
|
||||
if cls in ("medium", "heavy-in-bursts") and not note[0]:
|
||||
fail("%s is %s but has no CPU note here" % (slug, cls))
|
||||
rec.update(typical_mb=s["mem_typical_mb"], peak_mb=s["mem_peak_mb"], cpu_class=cls,
|
||||
cpu_note_hu=note[0] if cls != "light" else "", cpu_note_en=note[1] if cls != "light" else "",
|
||||
measured=s["source"] != "unmeasured")
|
||||
apps.append(rec)
|
||||
return dict(
|
||||
schema=1,
|
||||
measured="2026-10-13",
|
||||
measured_on="Intel N100 mini PC, 16 GB (demo-felhom)",
|
||||
source=dict(path="documentation/audits/sizing-2026-10-13/sizing.json", commit=SOURCE_COMMIT,
|
||||
sha256=hashlib.sha256(raw).hexdigest()),
|
||||
platform_mb=PLATFORM_MB,
|
||||
headroom=HEADROOM,
|
||||
guest_rule=GUEST_RULE,
|
||||
boxes_gb=BOXES_GB,
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
def render(d):
|
||||
return json.dumps(d, ensure_ascii=False, indent=1) + "\n"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
out = render(build())
|
||||
if "--check" in sys.argv:
|
||||
cur = io.open(OUT, encoding="utf-8").read() if os.path.exists(OUT) else ""
|
||||
if cur != out:
|
||||
print("build_sizing_web: website/assets/sizing-data.json is NOT what the builder makes — rebuild it")
|
||||
sys.exit(1)
|
||||
print("build_sizing_web: sizing-data.json matches a fresh build")
|
||||
else:
|
||||
io.open(OUT, "w", encoding="utf-8", newline="\n").write(out)
|
||||
print("wrote", os.path.relpath(OUT, ROOT), "-", len(json.loads(out)["apps"]), "apps")
|
||||
Reference in New Issue
Block a user