#!/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, 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 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'