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

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:
2026-10-13 14:31:44 +02:00
parent fdca4971af
commit d5ef93b13b
14 changed files with 1813 additions and 15 deletions
+5 -1
View File
@@ -2,7 +2,11 @@
**Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.**
**Updated 2026-10-12 (evening): demo-hp, demo-felhom and Tester 1 run agent 0.154.0 and controller 0.309.0. The open-items list is at 124 (2026-10-13; the newest rows are the sizing bench, R-993..R-995, and a push-hook test defect, R-996). Reports: `REPORT-usenet-2026-10-12.md`, `REPORT-newapps-2026-10-12.md`, `REPORT-disk-evening-2026-10-12.md`, `REPORT-answers-2026-10-12.md`, `REPORT-m3-2026-10-11.md`, `REPORT-redesign-2026-10-10.md`, `REPORT-testers-2026-10-10.md`, `REPORT-register-shrink-2026-10-10.md`, `REPORT-new-apps-2026-10-10.md`, `REPORT-release-2026-10-10.md`, `REPORT-break-the-circle-2026-10-09.md`, `REPORT-dooplex-survival-2026-10-09.md`, `REPORT-day4-2026-10-09.md`.**
**Updated 2026-10-12 (evening): demo-hp, demo-felhom and Tester 1 run agent 0.154.0 and controller 0.309.0. The open-items list is at 125 (2026-10-13; the newest row is the machine estimator to try, R-997). Reports: `REPORT-usenet-2026-10-12.md`, `REPORT-newapps-2026-10-12.md`, `REPORT-disk-evening-2026-10-12.md`, `REPORT-answers-2026-10-12.md`, `REPORT-m3-2026-10-11.md`, `REPORT-redesign-2026-10-10.md`, `REPORT-testers-2026-10-10.md`, `REPORT-register-shrink-2026-10-10.md`, `REPORT-new-apps-2026-10-10.md`, `REPORT-release-2026-10-10.md`, `REPORT-break-the-circle-2026-10-09.md`, `REPORT-dooplex-survival-2026-10-09.md`, `REPORT-day4-2026-10-09.md`.**
## Tuesday 2026-10-13 (afternoon): the machine estimator is built — live, but unlisted
- **Waiting on you — decide:** try https://felhom.eu/gepigeny (English: https://felhom.eu/en/sizing), tick a few apps, and say „list it” or what to change (R-997). If you do nothing: the page stays unlisted and nobody finds it.
## Tuesday 2026-10-13: what a Felhom box needs — measured
@@ -159,8 +159,23 @@ and the page cache is what makes a photo library feel fast.
**Disk, why:** the installer gives the guest a 32 GB system volume and, from the free space, a 70 / 130 / 250 GB data
volume; the host keeps three daily guest dumps (19 GB on this box). Image sizes per app were **not measured** in this run.
**What each size runs** (the installer gives the guest: 8 GB → 4 GB, 16 GB → ≈ 11.6 GB, 32 GB → ≈ 27.6 GB):
- **8 GB:** the household set **without Immich** (peaks 2.0 GB + platform) with room to spare; Immich only on its own or with
one or two light apps (its import alone peaks at 3.1 GB of the guest's 4 GB). The floor of the offer.
- **16 GB:** the whole household set at peak (≈ 5.5 GB) twice over, plus a dozen light apps. The recommended box.
- **32 GB:** the household set plus most of the catalog: all 62 installable apps typical together ≈ 13.3 GB, at peak ≈ 21.3 GB.
**What each size runs — three verdicts, the same rule as the estimator page** (`/gepigeny`, `website/assets/sizing.js`).
*Corrected 2026-10-13:* the lines here first said an 8 GB box runs Immich „on its own” while this section sizes a box at
peak × 1.5 — which Immich alone does not meet on 8 GB ((470 + 3,061) × 1.5 ≈ 5.2 GB > 4 GB), though its peak fits
(3.4 GB). Three verdicts say both honestly. The guest is the installer's rule on MemTotal = label − 612 MiB (the
16 GB N100 reports 15,772 MiB): **8 GB → 4.0 GB, 16 GB → 11.4 GB, 32 GB → 27.4 GB** (the first version said 11.6 / 27.6).
**Fits well:** peak × 1.5 ≤ guest · **fits, but tight:** peak ≤ guest < peak × 1.5 — works, but a large photo import or
a backup at the same time can slow it down · **does not fit:** guest < peak. Peak = 470 MB platform + the apps' peaks summed.
<!-- sizing-verdicts: generated from the rule; scripts/test_build_sizing_web.py checks this table -->
| Selection | Peak | 8 GB | 16 GB | 32 GB |
|---|---|---|---|---|
| Household set | 5.4 GB | does not fit | fits well | fits well |
| Household set without Immich | 2.4 GB | fits well | fits well | fits well |
| Immich alone | 3.4 GB | fits, but tight | fits well | fits well |
| All 62 apps | 21.3 GB | does not fit | does not fit | fits, but tight |
<!-- /sizing-verdicts -->
- **8 GB:** the household set without Immich fits well; Immich alone fits, but tight; the whole household set does not fit.
- **16 GB:** the whole household set fits well.
- **32 GB:** the household set fits well; all 62 apps at once fit, but tight.
File diff suppressed because one or more lines are too long
+16
View File
@@ -1,3 +1,19 @@
## 2026-10-13 — `build_sizing_web.py` + `site_gates.py` UNLISTED pairs and gate 25
- **`build_sizing_web.py`** builds `website/assets/sizing-data.json` (the machine estimator's data) from
`documentation/audits/sizing-2026-10-13/sizing.json` and the two apps pages. Refuses (exit 1, writes nothing)
when a listed app is missing from `sizing.json` (FileBrowser excepted by name: built in), when a peak is below
its typical figure, or when the two apps pages disagree. `--check` compares the committed file with a fresh build.
- **`test_build_sizing_web.py`** (7 tests): fresh build = committed file; the four cases of SIZING.md §5 give the
expected verdicts; SIZING.md's verdict table agrees with the rule; `sizing.js` carries the rule's shape and none
of its numbers. Red-proofed: a hard-coded 470 in the JS, a changed headroom in the data, and a wrong table cell
each convicted. No JS runtime exists on DooPlex or in CI, so the rule is mirrored in Python on the same data.
- **`site_gates.py`**: `UNLISTED` twin pairs get every twin check (hreflang, globe, canonical, lang, no Hungarian
on the English page) and are excluded from the sitemap requirement; **gate 25** adds what unlisted means —
`noindex`, a `Disallow:` line per URL in `robots.txt`, absent from `sitemap.xml`, linked from no other page.
Red-proofed: each of the four, and a wrong twin alternate, convicted on its own; the first run caught the missing
`robots.txt` lines for real.
## 2026-10-12 (night) — make_hero_clip.py: scenes keep their own length
- **`--seconds` is no longer a fixed window.** Each scene now plays its full recorded length, read
+137
View File
@@ -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("&amp;", "&").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")
+44 -6
View File
@@ -109,6 +109,15 @@ URL = {"index.html": "/", "alkalmazasok.html": "/alkalmazasok", "technologiak.ht
# English twin is written then, as a twin.
NO_TWIN = {"szolgaltatasok-nonpublic.html", "404.html", "adatkezeles.html", "feltetelek.html"}
# UNLISTED (2026-10-13, the machine estimator) — a twin pair that is LIVE but deliberately unlisted until the
# operator says "list it" (R-997). It gets every twin check (hreflang, globe, canonical, lang, no Hungarian on the
# English page), and gate 25 adds what unlisted means: `noindex` on both pages, a `Disallow:` line for each URL in
# robots.txt, absent from sitemap.xml, and linked from no other page. To LIST it: move the pair into TWINS, drop
# noindex and the Disallow lines, add both URLs to the sitemap and a link from the menu — in one commit.
UNLISTED = {"gepigeny.html": os.path.join("en", "sizing.html")}
URL.update({"gepigeny.html": "/gepigeny", os.path.join("en", "sizing.html"): "/en/sizing"})
ALL_TWINS = dict(TWINS, **UNLISTED)
MOCKUPS = set() # the design studies were deleted with website/preview/ at the M3 switch
# SHELL (2026-10-11, M3) — every public page now wears the controller's frame instead of the
@@ -138,10 +147,12 @@ SHELL = {
os.path.join("en", 'testing.html'),
os.path.join("en", 'contact.html'),
os.path.join("en", 'download.html'),
'gepigeny.html',
os.path.join("en", 'sizing.html'),
}
PAGES = list(TWINS) + list(TWINS.values()) + sorted(NO_TWIN) + sorted(MOCKUPS) + sorted(SHELL)
EN_PAGES = set(TWINS.values())
PAGES = list(ALL_TWINS) + list(ALL_TWINS.values()) + sorted(NO_TWIN) + sorted(MOCKUPS) + sorted(SHELL)
EN_PAGES = set(ALL_TWINS.values())
HU_PAGES = set(PAGES) - EN_PAGES - MOCKUPS - SHELL
ANALYTICS_EXEMPT = {"szolgaltatasok-nonpublic.html"} | MOCKUPS
@@ -425,7 +436,7 @@ def check_globe(p, lang, urls=None):
ALT_RE = re.compile(r'<link rel="alternate" hreflang="([^"]+)" href="([^"]+)">')
for hu, en in TWINS.items():
for hu, en in ALL_TWINS.items():
if hu not in pages or en not in pages:
fail("twin pair %s <-> %s: a page of the pair is missing" % (hu, en))
continue
@@ -438,7 +449,7 @@ for hu, en in TWINS.items():
canon = re.search(r'<link rel="canonical" href="([^"]+)">', pages[p])
if not canon or canon.group(1) != SITE + URL[p]:
fail("%s: canonical %r — it must point at the page itself (%s)" % (p, canon and canon.group(1), SITE + URL[p]))
print(" twins: %d pairs checked both ways (hreflang, globe, canonical)" % len(TWINS))
print(" twins: %d pairs checked both ways (hreflang, globe, canonical), %d of them unlisted" % (len(ALL_TWINS), len(UNLISTED)))
for p in sorted(NO_TWIN): # no twin: the globe's structure, current mark and label still hold
if p in pages:
check_globe(p, lang_of(p))
@@ -546,7 +557,7 @@ for p in sorted(x for x in pages if is_en(x)):
if BEG.search(t):
fail("%s: the site does not beg — no \"please\"/\"kindly\": %r" % (p, t[:120]))
# the retrieval promise: the English page may make it no more often than its Hungarian twin does
hu = next((h for h, e in TWINS.items() if e == p), None)
hu = next((h for h, e in ALL_TWINS.items() if e == p), None)
if hu and hu in pages:
en_n = sum(len(re.findall(rx, t, re.I)) for t in visible_strings(s) for rx in _RET_EN)
hu_n = sum(fold(t).count(fold(st)) for t in visible_strings(pages[hu]) for st in _RET_HU)
@@ -817,7 +828,8 @@ if not os.path.exists(_SITEMAP):
else:
_sm = io.open(_SITEMAP, encoding="utf-8-sig").read()
_locs = [re.sub(r"^https?://felhom\.eu", "", u) for u in re.findall(r"<loc>([^<]+)</loc>", _sm)]
_want = set(URL.values()) | _LEGAL_URLS
_unlisted_urls = {URL[_p] for _pair in UNLISTED.items() for _p in _pair}
_want = (set(URL.values()) - _unlisted_urls) | _LEGAL_URLS
_have = set(_locs)
for _u in sorted(_want - _have):
fail("sitemap.xml: %s is a public page and is NOT listed — search engines are told the site "
@@ -844,6 +856,32 @@ else:
if not [f for f in fails if "sitemap.xml" in f]:
print(" sitemap: %d URL(s), every public page once, every twin pair cross-named" % len(_locs))
# ---------------------------------------------------------------------------------------------
# gate 25 (2026-10-13): an UNLISTED pair stays unlisted until the operator lists it (R-997). Live and reachable by its
# link, but: noindex on the page, Disallow in robots.txt, absent from sitemap.xml, and no other page links to it —
# each of the four is what "unlisted" means, and losing any one of them quietly publishes the page.
_ROBOTS = os.path.join(W, "robots.txt")
_robots = io.open(_ROBOTS, encoding="utf-8-sig").read() if os.path.exists(_ROBOTS) else ""
_sm_locs = set(re.sub(r"^https?://felhom\.eu", "", u) for u in re.findall(r"<loc>([^<]+)</loc>", io.open(_SITEMAP, encoding="utf-8-sig").read())) if os.path.exists(_SITEMAP) else set()
_unl_pages = {p for _pair in UNLISTED.items() for p in _pair}
for _p in sorted(_unl_pages):
_s = pages.get(_p, "")
_u = URL[_p]
_m = re.search(r'<meta name="robots" content="([^"]*)">', _s)
if not _m or "noindex" not in [x.strip() for x in _m.group(1).split(",")]:
fail("%s: an UNLISTED page without <meta name=\"robots\" content=\"noindex\"> — search engines may show it" % _p)
if not re.search(r"(?m)^Disallow:\s*%s\s*$" % re.escape(_u), _robots):
fail("robots.txt: no 'Disallow: %s' — the UNLISTED page %s is open to crawlers" % (_u, _p))
if _u in _sm_locs:
fail("sitemap.xml: %s is listed — an UNLISTED page must not be in the sitemap" % _u)
_link = re.compile(r'href="(?:https?://felhom\.eu)?%s(?:[#?"])' % re.escape(_u))
for _q in sorted(pages):
if _q in _unl_pages:
continue # the pair's own globe, canonical and alternates point at each other
if _link.search(pages[_q]):
fail("%s links to the UNLISTED page %s — it is published by that link" % (_q, _u))
print(" unlisted: %d page(s) — noindex, robots Disallow, not in the sitemap, linked from no other page" % len(_unl_pages))
if fails:
print("\nSITE GATES FAILED: %d problem(s)" % len(fails))
sys.exit(1)
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Tests for the machine estimator's data and rule (website/assets/sizing-data.json, sizing.js).
No JavaScript runtime exists on DooPlex or in CI (python3 + git only), so the rule is checked here in Python, on
the SAME data file the page reads, and the JavaScript is held to having no numbers of its own:
1. the committed data file is exactly what scripts/build_sizing_web.py builds
2. the four cases of SIZING.md §5 give the expected verdicts by the rule (estimate() below mirrors sizing.js)
3. the verdict table in SIZING.md §5 says the same as the rule computes
4. sizing.js carries the rule's shape and none of its constants (they all come from the data file)
Run: python3 -I scripts/test_build_sizing_web.py
"""
import io, json, os, re, sys, unittest
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, "scripts"))
import build_sizing_web as B
DATA = os.path.join(ROOT, "website", "assets", "sizing-data.json")
JS = os.path.join(ROOT, "website", "assets", "sizing.js")
SIZING_MD = os.path.join(ROOT, "documentation", "audits", "sizing-2026-10-13", "SIZING.md")
HOUSEHOLD = ["immich", "jellyfin", "nextcloud", "home-assistant", "vaultwarden", "paperless-ngx"]
WORDS = {"well": "fits well", "tight": "fits, but tight", "no": "does not fit"}
def guest_mib(d, gb): # mirrors sizing.js guestMib()
r = d["guest_rule"]
total = gb * 1024 - r["memtotal_shortfall_mib"]
mem = total - r["reserve_mib"]
if mem > total - r["max_gap_mib"]:
mem = total - r["max_gap_mib"]
if mem < r["min_mib"]:
mem = r["min_mib"]
if mem > total - r["ceiling_gap_mib"]:
mem = total - r["ceiling_gap_mib"]
return mem
def estimate(d, slugs): # mirrors sizing.js estimate()
by = {a["slug"]: a for a in d["apps"]}
peak = d["platform_mb"] + sum(by[s]["peak_mb"] for s in slugs if not by[s].get("builtin"))
out = []
for gb in d["boxes_gb"]:
g = guest_mib(d, gb)
out.append("well" if peak * d["headroom"] <= g else ("tight" if peak <= g else "no"))
return peak, out
class SizingWeb(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.d = json.load(io.open(DATA, encoding="utf-8"))
cls.all = [a["slug"] for a in cls.d["apps"] if not a.get("builtin")]
def test_committed_file_is_a_fresh_build(self):
self.assertEqual(io.open(DATA, encoding="utf-8").read(), B.render(B.build()),
"sizing-data.json differs from a fresh build — run scripts/build_sizing_web.py")
def test_offers_the_62_listed_apps_plus_builtin(self):
self.assertEqual(len(self.all), 62)
self.assertEqual([a["slug"] for a in self.d["apps"] if a.get("builtin")], ["filebrowser"])
def test_guest_sizes_follow_the_installer(self):
self.assertEqual([guest_mib(self.d, gb) for gb in (8, 16, 32)], [4096, 11676, 28060])
self.assertEqual(guest_mib(self.d, 4), 4 * 1024 - 612 - 1024) # a 4 GB host: the ceiling, not 4 GB
def test_the_four_cases(self):
cases = {
"household": (HOUSEHOLD, ["no", "well", "well"]),
"household without immich": ([s for s in HOUSEHOLD if s != "immich"], ["well", "well", "well"]),
"immich alone": (["immich"], ["tight", "well", "well"]),
"all 62": (self.all, ["no", "no", "tight"]),
"nothing": ([], ["well", "well", "well"]),
}
for name, (sel, want) in cases.items():
self.assertEqual(estimate(self.d, sel)[1], want, name)
def test_sizing_md_table_agrees_with_the_rule(self):
with io.open(SIZING_MD, encoding="utf-8") as f:
s = f.read()
block = re.search(r"<!-- sizing-verdicts:.*?-->(.*?)<!-- /sizing-verdicts -->", s, re.S)
self.assertIsNotNone(block, "SIZING.md has no sizing-verdicts table")
rows = {}
for line in block.group(1).splitlines():
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if len(cells) == 5 and cells[0] not in ("Selection", "---"):
rows[cells[0]] = cells
sel = {"Household set": HOUSEHOLD, "Household set without Immich": [s for s in HOUSEHOLD if s != "immich"],
"Immich alone": ["immich"], "All 62 apps": self.all}
self.assertEqual(set(rows), set(sel))
for name, slugs in sel.items():
peak, verdicts = estimate(self.d, slugs)
self.assertEqual(rows[name][1], "%.1f GB" % (peak / 1024), name + " peak")
self.assertEqual(rows[name][2:], [WORDS[v] for v in verdicts], name)
def test_js_has_the_rule_and_none_of_its_numbers(self):
js = io.open(JS, encoding="utf-8").read()
for need in ("data.platform_mb", "data.headroom", "r.reserve_mib", "r.max_gap_mib", "r.min_mib",
"r.ceiling_gap_mib", "r.memtotal_shortfall_mib", "data.boxes_gb",
"peak * data.headroom <= guest", "peak <= guest"):
self.assertIn(need, js)
code = re.sub(r"/\*.*?\*/", "", js, flags=re.S)
code = re.sub(r"(?m)//.*$", "", code)
code = re.sub(r"'(?:[^'\\]|\\.)*'", "''", code)
nums = set(re.findall(r"(?<![\w.])\d+(?:\.\d+)?", code))
self.assertTrue(nums <= {"0", "1", "2", "1024"}, "sizing.js holds its own numbers: %s" % sorted(nums - {"0", "1", "2", "1024"}))
def test_data_has_no_peak_below_typical(self):
for a in self.d["apps"]:
self.assertGreaterEqual(a["peak_mb"], a["typical_mb"], a["slug"])
if __name__ == "__main__":
unittest.main(verbosity=2)
+20 -1
View File
@@ -1,4 +1,23 @@
## 2026-10-12 (late night) — the Usenet pair on the app page, and what a subscription means
## 2026-10-13 — the machine estimator, live but unlisted (`/gepigeny`, `/en/sizing`)
A household ticks the apps it wants and sees, for an 8, 16 and 32 GB box: **fits well**, **fits, but tight**
or **does not fit** — in text, an icon and a colour, never colour alone — the typical and peak totals, and the
apps that make the CPU work hard for a short time. Numbers: the 2026-10-13 N100 measurement
(`documentation/audits/sizing-2026-10-13/`). R-997.
- **`assets/sizing.js`** — the rule in one function (`estimate()` + `guestMib()`), every number from
`assets/sizing-data.json`; the page text lives in each page (`#sizer-strings`), so one file serves both languages.
Vanilla JS, nothing loaded from another server.
- **`assets/sizing-data.json`** — built by `scripts/build_sizing_web.py` from `sizing.json` and the two apps
pages: the 62 listed apps plus FileBrowser, shown as built in (its memory is inside the 470 MB platform).
- **`assets/sizing.css`** — every selector under `.sizer`, so `site.css` and the shell are untouched (no
site-wide `?v=` bump needed).
- **Unlisted:** `noindex` on both pages, `Disallow: /gepigeny` and `Disallow: /en/sizing` in `robots.txt`, not
in `sitemap.xml`, linked from no page. `site_gates.py` gate 25 enforces all four.
- **No promise:** the pages name no minimum machine (R-993 is the operator's) and give no power or noise figure.
- **Not run in a browser by CC** — DooPlex has no browser and no JS engine; the operator's try is the first run.
## 2026-10-12 (late night) — the Usenet pair on the app page, and what a subscription means
The catalog gained SABnzbd and NZBGet (`app-catalog-felhom.eu` CHANGELOG, same day, R-988). The
website follows them.
+941
View File
@@ -0,0 +1,941 @@
{
"schema": 1,
"measured": "2026-10-13",
"measured_on": "Intel N100 mini PC, 16 GB (demo-felhom)",
"source": {
"path": "documentation/audits/sizing-2026-10-13/sizing.json",
"commit": "c885b5ac",
"sha256": "fa5e61dae02250e1abaab5506e66a5ad03c6bb87c17bc699bb4121d3fb2db401"
},
"platform_mb": 470,
"headroom": 1.5,
"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
],
"categories": [
{
"id": "fajlkezeles",
"id_en": "files",
"name_hu": "Fájlkezelés & Irodai",
"name_en": "Files & Office"
},
{
"id": "media",
"id_en": "media",
"name_hu": "Média & Szórakozás",
"name_en": "Media & Entertainment"
},
{
"id": "otthon",
"id_en": "home",
"name_hu": "Otthon & Életmód",
"name_en": "Home & Lifestyle"
},
{
"id": "produktivitas",
"id_en": "productivity",
"name_hu": "Produktivitás & Tudáskezelés",
"name_en": "Productivity & Knowledge"
},
{
"id": "rendszer",
"id_en": "system",
"name_hu": "Rendszer & Fejlesztés",
"name_en": "System & Development"
}
],
"apps": [
{
"slug": "nextcloud",
"name_hu": "Nextcloud",
"name_en": "Nextcloud",
"tagline_hu": "A saját Google Drive-od",
"tagline_en": "Your own Google Drive",
"category": "fajlkezeles",
"typical_mb": 314,
"peak_mb": 525,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "filebrowser",
"name_hu": "FileBrowser",
"name_en": "FileBrowser",
"tagline_hu": "Egyszerű webes fájlkezelő",
"tagline_en": "Simple web file manager",
"category": "fajlkezeles",
"builtin": true,
"typical_mb": 0,
"peak_mb": 0,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "gokapi",
"name_hu": "Gokapi",
"name_en": "Gokapi",
"tagline_hu": "Ideiglenes fájlmegosztás",
"tagline_en": "Temporary file sharing",
"category": "fajlkezeles",
"typical_mb": 7,
"peak_mb": 77,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "zipline",
"name_hu": "Zipline",
"name_en": "Zipline",
"tagline_hu": "ShareX/Flameshot szerver",
"tagline_en": "ShareX/Flameshot server",
"category": "fajlkezeles",
"typical_mb": 196,
"peak_mb": 217,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "onlyoffice",
"name_hu": "OnlyOffice",
"name_en": "OnlyOffice",
"tagline_hu": "Irodai csomag böngészőben",
"tagline_en": "An office suite in the browser",
"category": "fajlkezeles",
"typical_mb": 364,
"peak_mb": 488,
"cpu_class": "medium",
"cpu_note_hu": "csak az első indításkor",
"cpu_note_en": "only at first start",
"measured": true
},
{
"slug": "bentopdf",
"name_hu": "BentoPDF",
"name_en": "BentoPDF",
"tagline_hu": "PDF eszköztár (adatvédelem)",
"tagline_en": "PDF toolkit (privacy first)",
"category": "fajlkezeles",
"typical_mb": 5,
"peak_mb": 10,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "papra",
"name_hu": "Papra",
"name_en": "Papra",
"tagline_hu": "Minimalista dokumentumtár",
"tagline_en": "A minimal document archive",
"category": "fajlkezeles",
"typical_mb": 266,
"peak_mb": 405,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "immich",
"name_hu": "Immich",
"name_en": "Immich",
"tagline_hu": "Google Photos alternatíva",
"tagline_en": "A Google Photos alternative",
"category": "media",
"typical_mb": 1356,
"peak_mb": 3061,
"cpu_class": "heavy-in-bursts",
"cpu_note_hu": "csak amíg új fotókat dolgoz fel (bélyegképek, arcok, keresés): 200 fotó kb. 6 perc",
"cpu_note_en": "only while it processes new photos (thumbnails, faces, search): 200 photos took about 6 minutes",
"measured": true
},
{
"slug": "jellyfin",
"name_hu": "Jellyfin",
"name_en": "Jellyfin",
"tagline_hu": "Média szerver (Netflix)",
"tagline_en": "Media server (Netflix)",
"category": "media",
"typical_mb": 191,
"peak_mb": 373,
"cpu_class": "heavy-in-bursts",
"cpu_note_hu": "csak amíg egy filmet átalakít egy olyan eszköznek, amely azt nem tudja lejátszani",
"cpu_note_en": "only while it converts a film for a device that cannot play it",
"measured": true
},
{
"slug": "plex",
"name_hu": "Plex",
"name_en": "Plex",
"tagline_hu": "Média szerver (prémium)",
"tagline_en": "Media server (premium)",
"category": "media",
"typical_mb": 232,
"peak_mb": 234,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "emby",
"name_hu": "Emby",
"name_en": "Emby",
"tagline_hu": "Média szerver",
"tagline_en": "Media server",
"category": "media",
"typical_mb": 142,
"peak_mb": 146,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "navidrome",
"name_hu": "Navidrome",
"name_en": "Navidrome",
"tagline_hu": "Zene szerver (Spotify)",
"tagline_en": "Music server (Spotify)",
"category": "media",
"typical_mb": 14,
"peak_mb": 18,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "audiobookshelf",
"name_hu": "Audiobookshelf",
"name_en": "Audiobookshelf",
"tagline_hu": "Hangoskönyv & podcast",
"tagline_en": "Audiobooks & podcasts",
"category": "media",
"typical_mb": 48,
"peak_mb": 54,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "metube",
"name_hu": "MeTube",
"name_en": "MeTube",
"tagline_hu": "Videók letöltése linkből",
"tagline_en": "Download videos from a link",
"category": "media",
"typical_mb": 62,
"peak_mb": 329,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "qbittorrent",
"name_hu": "qBittorrent",
"name_en": "qBittorrent",
"tagline_hu": "Torrent letöltő",
"tagline_en": "Torrent downloader",
"category": "media",
"typical_mb": 163,
"peak_mb": 318,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "transmission",
"name_hu": "Transmission",
"name_en": "Transmission",
"tagline_hu": "Egyszerű torrent letöltő",
"tagline_en": "A simple torrent downloader",
"category": "media",
"typical_mb": 6,
"peak_mb": 22,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "sabnzbd",
"name_hu": "SABnzbd",
"name_en": "SABnzbd",
"tagline_hu": "Usenet letöltő",
"tagline_en": "Usenet downloader",
"category": "media",
"typical_mb": 68,
"peak_mb": 68,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "nzbget",
"name_hu": "NZBGet",
"name_en": "NZBGet",
"tagline_hu": "Egyszerű Usenet letöltő",
"tagline_en": "A simple Usenet downloader",
"category": "media",
"typical_mb": 5,
"peak_mb": 5,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "calibre-web",
"name_hu": "Calibre-Web Automated",
"name_en": "Calibre-Web Automated",
"tagline_hu": "E-könyv könyvtár",
"tagline_en": "E-book library",
"category": "media",
"typical_mb": 191,
"peak_mb": 413,
"cpu_class": "medium",
"cpu_note_hu": "csak amíg új könyveket vesz fel",
"cpu_note_en": "only while it imports new books",
"measured": true
},
{
"slug": "komga",
"name_hu": "Komga",
"name_en": "Komga",
"tagline_hu": "Képregény & manga szerver",
"tagline_en": "Comics & manga server",
"category": "media",
"typical_mb": 442,
"peak_mb": 496,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "grimmory",
"name_hu": "Grimmory",
"name_en": "Grimmory",
"tagline_hu": "E-könyvtár böngészőben",
"tagline_en": "E-book library in the browser",
"category": "media",
"typical_mb": 346,
"peak_mb": 1095,
"cpu_class": "medium",
"cpu_note_hu": "csak az első indításkor",
"cpu_note_en": "only at first start",
"measured": true
},
{
"slug": "sonarr",
"name_hu": "Sonarr",
"name_en": "Sonarr",
"tagline_hu": "Sorozat menedzser",
"tagline_en": "Series manager",
"category": "media",
"typical_mb": 77,
"peak_mb": 77,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "radarr",
"name_hu": "Radarr",
"name_en": "Radarr",
"tagline_hu": "Film menedzser",
"tagline_en": "Film manager",
"category": "media",
"typical_mb": 96,
"peak_mb": 100,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "seerr",
"name_hu": "Seerr",
"name_en": "Seerr",
"tagline_hu": "Média kérés kezelő",
"tagline_en": "Media request manager",
"category": "media",
"typical_mb": 166,
"peak_mb": 168,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "romm",
"name_hu": "RoMM",
"name_en": "RoMM",
"tagline_hu": "ROM gyűjtemény kezelő",
"tagline_en": "ROM collection manager",
"category": "media",
"typical_mb": 528,
"peak_mb": 769,
"cpu_class": "medium",
"cpu_note_hu": "az első indításkor; a játékkönyvtár átnézését nem mértük",
"cpu_note_en": "at first start; a library scan was not measured",
"measured": true
},
{
"slug": "crafty-controller",
"name_hu": "Crafty Controller",
"name_en": "Crafty Controller",
"tagline_hu": "Minecraft szerver kezelő",
"tagline_en": "Minecraft server manager",
"category": "media",
"typical_mb": 78,
"peak_mb": 84,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "home-assistant",
"name_hu": "Home Assistant",
"name_en": "Home Assistant",
"tagline_hu": "Okos otthon központ",
"tagline_en": "Smart home hub",
"category": "otthon",
"typical_mb": 318,
"peak_mb": 328,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "n8n",
"name_hu": "n8n",
"name_en": "n8n",
"tagline_hu": "Workflow automatizálás",
"tagline_en": "Workflow automation",
"category": "otthon",
"typical_mb": 321,
"peak_mb": 550,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "tandoor",
"name_hu": "Tandoor Recipes",
"name_en": "Tandoor Recipes",
"tagline_hu": "Receptkezelő",
"tagline_en": "Recipe manager",
"category": "otthon",
"typical_mb": 868,
"peak_mb": 868,
"cpu_class": "medium",
"cpu_note_hu": "csak az első indításkor",
"cpu_note_en": "only at first start",
"measured": true
},
{
"slug": "mealie",
"name_hu": "Mealie",
"name_en": "Mealie",
"tagline_hu": "Receptek és étkezésterv",
"tagline_en": "Recipes and meal plans",
"category": "otthon",
"typical_mb": 273,
"peak_mb": 431,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "recipe-importer",
"name_hu": "Recipe Importer",
"name_en": "Recipe Importer",
"tagline_hu": "Magyar receptoldalakról",
"tagline_en": "From Hungarian recipe sites",
"category": "otthon",
"typical_mb": 63,
"peak_mb": 72,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "wishlist",
"name_hu": "Wishlist",
"name_en": "Wishlist",
"tagline_hu": "Családi kívánságlista",
"tagline_en": "Family wish list",
"category": "otthon",
"typical_mb": 115,
"peak_mb": 184,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "homebox",
"name_hu": "Homebox",
"name_en": "Homebox",
"tagline_hu": "Otthoni leltár",
"tagline_en": "Home inventory",
"category": "otthon",
"typical_mb": 17,
"peak_mb": 118,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "gramps-web",
"name_hu": "Gramps Web",
"name_en": "Gramps Web",
"tagline_hu": "Családfa készítő",
"tagline_en": "Family tree builder",
"category": "otthon",
"typical_mb": 326,
"peak_mb": 342,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "sparkyfitness",
"name_hu": "SparkyFitness",
"name_en": "SparkyFitness",
"tagline_hu": "Táplálkozási napló",
"tagline_en": "Food diary",
"category": "otthon",
"typical_mb": 305,
"peak_mb": 664,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "actualbudget",
"name_hu": "Actual Budget",
"name_en": "Actual Budget",
"tagline_hu": "Költségvetés tervező",
"tagline_en": "Budget planner",
"category": "otthon",
"typical_mb": 44,
"peak_mb": 167,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "adventurelog",
"name_hu": "AdventureLog",
"name_en": "AdventureLog",
"tagline_hu": "Utazási napló",
"tagline_en": "Travel log",
"category": "otthon",
"typical_mb": 361,
"peak_mb": 464,
"cpu_class": "medium",
"cpu_note_hu": "csak az első indításkor",
"cpu_note_en": "only at first start",
"measured": true
},
{
"slug": "wanderer",
"name_hu": "Wanderer",
"name_en": "Wanderer",
"tagline_hu": "Túra tervező",
"tagline_en": "Hike planner",
"category": "otthon",
"typical_mb": 143,
"peak_mb": 147,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "dawarich",
"name_hu": "Dawarich",
"name_en": "Dawarich",
"tagline_hu": "A Google Idővonal helyett",
"tagline_en": "Instead of Google Timeline",
"category": "otthon",
"typical_mb": 390,
"peak_mb": 890,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "calcom",
"name_hu": "Cal.com",
"name_en": "Cal.com",
"tagline_hu": "Időpontfoglaló",
"tagline_en": "Appointment booking",
"category": "otthon",
"typical_mb": 800,
"peak_mb": 950,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "lubelogger",
"name_hu": "LubeLogger",
"name_en": "LubeLogger",
"tagline_hu": "Autónyilvántartás",
"tagline_en": "Vehicle records",
"category": "otthon",
"typical_mb": 53,
"peak_mb": 113,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "grocy",
"name_hu": "Grocy",
"name_en": "Grocy",
"tagline_hu": "Háztartásvezetés",
"tagline_en": "Household management",
"category": "otthon",
"typical_mb": 30,
"peak_mb": 114,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "vikunja",
"name_hu": "Vikunja",
"name_en": "Vikunja",
"tagline_hu": "Feladatkezelő (Todoist)",
"tagline_en": "Task manager (Todoist)",
"category": "produktivitas",
"typical_mb": 20,
"peak_mb": 104,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "radicale",
"name_hu": "Radicale",
"name_en": "Radicale",
"tagline_hu": "Naptár és névjegyek",
"tagline_en": "Calendar and contacts",
"category": "produktivitas",
"typical_mb": 27,
"peak_mb": 27,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "kimai",
"name_hu": "Kimai",
"name_en": "Kimai",
"tagline_hu": "Időkövetés",
"tagline_en": "Time tracking",
"category": "produktivitas",
"typical_mb": 163,
"peak_mb": 240,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "rallly",
"name_hu": "Rallly",
"name_en": "Rallly",
"tagline_hu": "Időpont szavazás (Doodle)",
"tagline_en": "Meeting poll (Doodle)",
"category": "produktivitas",
"typical_mb": 360,
"peak_mb": 479,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "claper",
"name_hu": "Claper",
"name_en": "Claper",
"tagline_hu": "Interaktív prezentációk",
"tagline_en": "Interactive presentations",
"category": "produktivitas",
"typical_mb": 248,
"peak_mb": 248,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "ghost",
"name_hu": "Ghost",
"name_en": "Ghost",
"tagline_hu": "Blog & hírlevél platform",
"tagline_en": "Blog & newsletter platform",
"category": "produktivitas",
"typical_mb": 149,
"peak_mb": 149,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "paperless-ngx",
"name_hu": "Paperless-ngx",
"name_en": "Paperless-ngx",
"tagline_hu": "Digitális irattár",
"tagline_en": "A digital filing cabinet",
"category": "produktivitas",
"typical_mb": 568,
"peak_mb": 659,
"cpu_class": "medium",
"cpu_note_hu": "csak amíg új beszkennelt dokumentumokat olvas be (szövegfelismerés, kb. 4 oldal percenként)",
"cpu_note_en": "only while it reads new scanned documents (text recognition, about 4 pages a minute)",
"measured": true
},
{
"slug": "bookstack",
"name_hu": "BookStack",
"name_en": "BookStack",
"tagline_hu": "Könyv-szerű dokumentáció",
"tagline_en": "Book-style documentation",
"category": "produktivitas",
"typical_mb": 159,
"peak_mb": 225,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "outline",
"name_hu": "Outline",
"name_en": "Outline",
"tagline_hu": "Csapat tudásbázis",
"tagline_en": "Team knowledge base",
"category": "produktivitas",
"typical_mb": 282,
"peak_mb": 393,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "docmost",
"name_hu": "Docmost",
"name_en": "Docmost",
"tagline_hu": "Notion-szerű wiki",
"tagline_en": "A Notion-like wiki",
"category": "produktivitas",
"typical_mb": 415,
"peak_mb": 495,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "karakeep",
"name_hu": "Karakeep",
"name_en": "Karakeep",
"tagline_hu": "Kereshető saját archívum",
"tagline_en": "Your own searchable archive",
"category": "produktivitas",
"typical_mb": 361,
"peak_mb": 1050,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "privatebin",
"name_hu": "PrivateBin",
"name_en": "PrivateBin",
"tagline_hu": "Titkosított jegyzet megosztás",
"tagline_en": "Encrypted note sharing",
"category": "produktivitas",
"typical_mb": 40,
"peak_mb": 59,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "vaultwarden",
"name_hu": "Vaultwarden",
"name_en": "Vaultwarden",
"tagline_hu": "Jelszókezelő (Bitwarden)",
"tagline_en": "Password manager (Bitwarden)",
"category": "rendszer",
"typical_mb": 44,
"peak_mb": 99,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "homepage",
"name_hu": "Homepage",
"name_en": "Homepage",
"tagline_hu": "Személyes kezdőlap",
"tagline_en": "Personal start page",
"category": "rendszer",
"typical_mb": 82,
"peak_mb": 83,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "glance",
"name_hu": "Glance",
"name_en": "Glance",
"tagline_hu": "Információs dashboard",
"tagline_en": "Information dashboard",
"category": "rendszer",
"typical_mb": 6,
"peak_mb": 7,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "uptime-kuma",
"name_hu": "Uptime Kuma",
"name_en": "Uptime Kuma",
"tagline_hu": "Szolgáltatás monitoring",
"tagline_en": "Service monitoring",
"category": "rendszer",
"typical_mb": 103,
"peak_mb": 227,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "grafana",
"name_hu": "Grafana",
"name_en": "Grafana",
"tagline_hu": "Adatvizualizáció",
"tagline_en": "Data visualisation",
"category": "rendszer",
"typical_mb": 220,
"peak_mb": 435,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "termix",
"name_hu": "Termix",
"name_en": "Termix",
"tagline_hu": "Szerver menedzser (SSH)",
"tagline_en": "Server manager (SSH)",
"category": "rendszer",
"typical_mb": 102,
"peak_mb": 117,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "gitea",
"name_hu": "Gitea",
"name_en": "Gitea",
"tagline_hu": "Git szerver (GitHub)",
"tagline_en": "Git server (GitHub)",
"category": "rendszer",
"typical_mb": 74,
"peak_mb": 136,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "code-server",
"name_hu": "Code-Server",
"name_en": "Code-Server",
"tagline_hu": "VS Code böngészőben",
"tagline_en": "VS Code in the browser",
"category": "rendszer",
"typical_mb": 59,
"peak_mb": 59,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
},
{
"slug": "opengist",
"name_hu": "OpenGist",
"name_en": "OpenGist",
"tagline_hu": "Kód snippetek (Gist)",
"tagline_en": "Code snippets (Gist)",
"category": "rendszer",
"typical_mb": 27,
"peak_mb": 68,
"cpu_class": "light",
"cpu_note_hu": "",
"cpu_note_en": "",
"measured": true
}
]
}
+41
View File
@@ -0,0 +1,41 @@
/* sizing.css — the machine estimator only (/gepigeny, /en/sizing). Every selector starts with .sizer,
so nothing here reaches the shell or another page. Tokens from site.css :root; no raw colours. */
.sizer[hidden] { display: none; }
.sizer-presets { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0 0 1.5rem; }
.sizer-layout { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); gap: 2.5rem; align-items: start; }
.sizer-group { border: 0; border-top: 1px solid var(--line); margin: 0 0 1.25rem; padding: 0.75rem 0 0; min-width: 0; }
.sizer-group legend { color: var(--text-1); font-weight: 600; padding: 0 0.5rem 0 0; }
.sizer-app { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.6rem; padding: 0.35rem 0; cursor: pointer; }
.sizer-app input { accent-color: var(--blue); width: 1.05rem; height: 1.05rem; margin: 0; flex: 0 0 auto; align-self: center; }
.sizer-app input:focus-visible { outline: 2px solid var(--blue-bright); outline-offset: 2px; }
.sizer-app input:disabled + .sizer-app-name { color: var(--text-2); }
.sizer-app-name { color: var(--text-1); }
.sizer-app-tag { color: var(--text-3); font-size: 0.85rem; }
.sizer-result h2 { font-size: 1.15rem; margin: 0 0 0.75rem; }
.sizer-totals { color: var(--text-2); margin: 0; line-height: 1.7; }
.sizer-num { font-family: var(--font-data); color: var(--text-1); font-weight: 600; }
.sizer-fine { color: var(--text-3); font-size: 0.85rem; margin: 0.25rem 0 0; }
.sizer-boxes { list-style: none; margin: 1rem 0; padding: 0; display: grid; gap: 0.75rem; }
.sizer-box { border: 1px solid var(--line); border-left-width: 3px; border-radius: var(--radius); padding: 0.75rem 1rem; background: var(--bg-1); }
.sizer-box-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.sizer-box-size { font-family: var(--font-data); color: var(--text-1); }
.sizer-verdict { color: var(--text-1); }
.sizer-well { border-left-color: var(--blue); }
.sizer-well .ico { color: var(--blue-bright); }
.sizer-tight { border-left-color: var(--warn); background: var(--warn-dim); }
.sizer-tight .ico, .sizer-tight .sizer-verdict { color: var(--warn); }
.sizer-no { border-left-color: var(--crit); background: var(--crit-dim); }
.sizer-no .ico, .sizer-no .sizer-verdict { color: var(--crit); }
.sizer-why { color: var(--text-2); font-size: 0.9rem; margin: 0.4rem 0 0; }
.sizer-sub { font-size: 1rem; margin: 1.25rem 0 0.5rem; }
.sizer-cpu { margin: 0; padding-left: 1.1rem; color: var(--text-2); line-height: 1.6; }
.sizer-cpu strong { color: var(--text-1); font-weight: 600; }
.sizer-advice { margin: 0; padding-left: 1.1rem; color: var(--text-2); line-height: 1.7; }
.sizer-how { margin-top: 1.5rem; color: var(--text-2); }
.sizer-how summary { cursor: pointer; color: var(--text-1); }
.sizer-how summary:focus-visible { outline: 2px solid var(--blue-bright); outline-offset: 2px; }
.sizer-how p { margin: 0.5rem 0 0; line-height: 1.65; }
.sizer-noscript { color: var(--warn); }
@media (max-width: 900px) {
.sizer-layout { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
}
+183
View File
@@ -0,0 +1,183 @@
/* sizing.js — the machine estimator (/gepigeny, /en/sizing).
*
* THE RULE lives in estimate() and guestMib() below, and nowhere else on the site. Every number it uses
* comes from /assets/sizing-data.json (built by scripts/build_sizing_web.py from the 2026-10-13 N100
* measurement); this file holds none of its own — scripts/test_build_sizing_web.py checks that.
*
* typical = platform + sum of the selected apps' typical figures
* peak = platform + sum of their peaks (all at peak at once: pessimistic on purpose)
* per box: guest memory by the installer's rule, then
* well peak * headroom <= guest
* tight peak <= guest < peak * headroom
* no guest < peak
*
* The page text is not here: each page carries its own strings (#sizer-strings), so this file is the
* same for both languages.
*/
(function (root) {
'use strict';
function guestMib(data, gb) {
var r = data.guest_rule;
var total = gb * 1024 - r.memtotal_shortfall_mib;
var mem = total - r.reserve_mib;
if (mem > total - r.max_gap_mib) { mem = total - r.max_gap_mib; }
if (mem < r.min_mib) { mem = r.min_mib; }
if (mem > total - r.ceiling_gap_mib) { mem = total - r.ceiling_gap_mib; }
return mem;
}
function estimate(data, slugs) {
var bySlug = {};
data.apps.forEach(function (a) { bySlug[a.slug] = a; });
var typical = data.platform_mb, peak = data.platform_mb, busy = [], unmeasured = [];
slugs.forEach(function (s) {
var a = bySlug[s];
if (!a || a.builtin) { return; }
typical += a.typical_mb;
peak += a.peak_mb;
if (a.cpu_class === 'medium' || a.cpu_class === 'heavy-in-bursts') { busy.push(a); }
if (!a.measured) { unmeasured.push(a); }
});
var boxes = data.boxes_gb.map(function (gb) {
var guest = guestMib(data, gb);
var verdict = peak * data.headroom <= guest ? 'well' : (peak <= guest ? 'tight' : 'no');
return { gb: gb, guest_mb: guest, verdict: verdict };
});
var heavy = busy.filter(function (a) { return a.cpu_class === 'heavy-in-bursts'; });
return { typical_mb: typical, peak_mb: peak, boxes: boxes, busy: busy, heavy: heavy, unmeasured: unmeasured };
}
root.FelhomSizing = { estimate: estimate, guestMib: guestMib };
/* ---------------------------------------------------------------- the page (drawing only) */
if (typeof document === 'undefined') { return; }
function el(tag, cls, txt) {
var e = document.createElement(tag);
if (cls) { e.className = cls; }
if (txt !== undefined) { e.textContent = txt; }
return e;
}
function icon(name) {
var ns = 'http://www.w3.org/2000/svg';
var s = document.createElementNS(ns, 'svg');
s.setAttribute('class', 'ico');
s.setAttribute('aria-hidden', 'true');
var u = document.createElementNS(ns, 'use');
u.setAttribute('href', '/assets/icons.svg?v=1#' + name);
s.appendChild(u);
return s;
}
function start() {
var host = document.getElementById('sizer');
var strEl = document.getElementById('sizer-strings');
if (!host || !strEl) { return; }
var T = JSON.parse(strEl.textContent);
var lang = document.documentElement.lang === 'en' ? 'en' : 'hu';
var gbText = function (mb) {
var v = (mb / 1024).toFixed(1);
return (lang === 'hu' ? v.replace('.', ',') : v) + ' GB';
};
var appsBox = document.getElementById('sizer-apps');
var result = document.getElementById('sizer-result');
fetch(host.getAttribute('data-src')).then(function (r) { return r.json(); }).then(function (data) {
data.categories.forEach(function (c) {
var fs = el('fieldset', 'sizer-group');
fs.appendChild(el('legend', '', lang === 'en' ? c.name_en : c.name_hu));
data.apps.filter(function (a) { return a.category === c.id; }).forEach(function (a) {
var lab = el('label', 'sizer-app');
var cb = el('input');
cb.type = 'checkbox';
cb.value = a.slug;
if (a.builtin) { cb.checked = true; cb.disabled = true; }
lab.appendChild(cb);
var name = el('span', 'sizer-app-name', lang === 'en' ? a.name_en : a.name_hu);
lab.appendChild(name);
var tag = a.builtin ? T.builtin : (lang === 'en' ? a.tagline_en : a.tagline_hu);
if (tag) { lab.appendChild(el('span', 'sizer-app-tag', tag)); }
fs.appendChild(lab);
});
appsBox.appendChild(fs);
});
function selected() {
return Array.prototype.map.call(appsBox.querySelectorAll('input[type=checkbox]:checked:not(:disabled)'),
function (c) { return c.value; });
}
function draw() {
var r = estimate(data, selected());
result.textContent = '';
var totals = el('p', 'sizer-totals');
totals.appendChild(el('span', '', T.typical + ': '));
totals.appendChild(el('strong', 'sizer-num', gbText(r.typical_mb)));
totals.appendChild(el('span', '', ' · ' + T.peak + ': '));
totals.appendChild(el('strong', 'sizer-num', gbText(r.peak_mb)));
result.appendChild(totals);
result.appendChild(el('p', 'sizer-fine', T.platform_note));
var ul = el('ul', 'sizer-boxes');
r.boxes.forEach(function (b) {
var li = el('li', 'sizer-box sizer-' + b.verdict);
var head = el('div', 'sizer-box-head');
head.appendChild(icon(b.verdict === 'well' ? 'i-check' : (b.verdict === 'tight' ? 'i-triangle-alert' : 'i-x')));
head.appendChild(el('strong', 'sizer-box-size', b.gb + ' GB'));
head.appendChild(el('span', 'sizer-verdict', T.verdict[b.verdict]));
li.appendChild(head);
li.appendChild(el('p', 'sizer-fine', T.guest.replace('{x}', gbText(b.guest_mb))));
if (b.verdict === 'tight') { li.appendChild(el('p', 'sizer-why', T.tight_why)); }
ul.appendChild(li);
});
result.appendChild(ul);
if (r.busy.length) {
result.appendChild(el('h3', 'sizer-sub', T.cpu_head));
var cl = el('ul', 'sizer-cpu');
r.busy.forEach(function (a) {
var li = el('li');
li.appendChild(el('strong', '', (lang === 'en' ? a.name_en : a.name_hu) + ': '));
li.appendChild(el('span', '', lang === 'en' ? a.cpu_note_en : a.cpu_note_hu));
cl.appendChild(li);
});
if (r.heavy.length >= 2) {
cl.appendChild(el('li', 'sizer-pair', T.heavy_pair.replace('{names}', r.heavy.map(function (a) {
return lang === 'en' ? a.name_en : a.name_hu;
}).join(T.and))));
}
result.appendChild(cl);
}
if (r.unmeasured.length) {
result.appendChild(el('p', 'sizer-why', T.unmeasured + ': ' + r.unmeasured.map(function (a) {
return lang === 'en' ? a.name_en : a.name_hu;
}).join(', ')));
}
}
appsBox.addEventListener('change', draw);
Array.prototype.forEach.call(document.querySelectorAll('[data-preset]'), function (btn) {
btn.addEventListener('click', function () {
var want = (btn.getAttribute('data-preset') || '').split(',').filter(Boolean);
Array.prototype.forEach.call(appsBox.querySelectorAll('input[type=checkbox]:not(:disabled)'), function (c) {
c.checked = want.indexOf(c.value) !== -1;
});
draw();
});
});
host.hidden = false;
draw();
}).catch(function () {
result.textContent = T.load_error;
host.hidden = false;
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
})(this);
+143
View File
@@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<script>document.documentElement.className=document.documentElement.className.replace('no-js','js');</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Which machine do I need? — an estimate for the apps you pick | Felhom.eu</title>
<meta name="description" content="Tick the apps you would run and see whether they fit an 8, 16 or 32 GB machine. An estimate from measurements on an N100 mini PC.">
<meta name="robots" content="noindex">
<link rel="canonical" href="https://felhom.eu/en/sizing">
<link rel="alternate" hreflang="hu" href="https://felhom.eu/gepigeny">
<link rel="alternate" hreflang="en" href="https://felhom.eu/en/sizing">
<link rel="alternate" hreflang="x-default" href="https://felhom.eu/gepigeny">
<link rel="preload" as="font" type="font/woff2" crossorigin href="/assets/fonts/pjs-latin.woff2">
<link rel="preload" as="font" type="font/woff2" crossorigin href="/assets/fonts/pjs-latin-ext.woff2">
<!-- shared assets: bump ?v= together on any site.css/icons.svg change -->
<link rel="stylesheet" href="/assets/site.css?v=30">
<link rel="stylesheet" href="/assets/shell.css?v=25">
<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>
<link rel="stylesheet" href="/assets/sizing.css?v=1">
<script defer src="/assets/sizing.js?v=1"></script>
</head>
<body class="shell page-gepigeny">
<header class="mobile-topbar">
<a href="/en/" class="mobile-topbar-logo"><img src="/assets/logo_notext_white.svg" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" id="navBurger" aria-expanded="false" aria-controls="sidebar" aria-label="Menu">
<span class="hamburger-box"><span class="hamburger-line"></span><span class="hamburger-line"></span><span class="hamburger-line"></span></span>
</button>
</header>
<div class="nav-backdrop" id="navBackdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<a href="/en/" class="sidebar-logo-link">
<img src="/assets/logo_notext_white.svg" alt="" class="sidebar-logo" aria-hidden="true">
<img src="/assets/logo-wordmark.png" alt="felhom.eu" class="sidebar-word">
</a>
</div>
<ul class="nav-links">
<li><a href="/en/"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-home"/></svg>Home</a></li>
<li><a href="/en/services"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-rocket"/></svg>Services</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-apps"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg>Applications<svg class="ico nav-chevron" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-chevron-down"/></svg></button>
<ul id="nav-group-apps" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/en/apps">About the apps</a></li>
<li><a href="/en/apps#files">Files &amp; Office</a></li>
<li><a href="/en/apps#media">Media &amp; Entertainment</a></li>
<li><a href="/en/apps#home">Home &amp; Lifestyle</a></li>
<li><a href="/en/apps#productivity">Productivity &amp; Knowledge</a></li>
<li><a href="/en/apps#system">System &amp; Development</a></li>
</ul>
</li>
<li><a href="/en/technology"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-layers"/></svg>Technology</a></li>
<li><a href="/en/backups"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-shield-check"/></svg>Backups</a></li>
</ul>
<div class="sidebar-bottom">
<a href="/en/testing" class="btn btn-primary sidebar-cta">Apply as a tester →</a>
<div class="nav-group-label">About Felhom</div>
<ul class="nav-links nav-links-sub">
<li><a href="/en/#how-it-works">How it works</a></li>
<li><a href="/en/faq">FAQ</a></li>
<li><a href="/en/contact">Contact</a></li>
</ul>
<div class="sidebar-footer">
<details class="lang-globe"><summary class="lang-globe-btn" aria-label="Language" title="Language"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-globe"/></svg></summary><ul class="lang-globe-menu"><li><a href="/gepigeny" lang="hu" hreflang="hu" class="lang-globe-item">Magyar</a></li><li><a href="/en/sizing" lang="en" hreflang="en" class="lang-globe-item current" aria-current="true">English</a></li></ul></details>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<h1>Which machine do I <span>need?</span></h1>
</div>
<p class="page-lede">Tick what you would run on the box, and the page shows how it fits an 8, 16 or 32 GB machine. This is an estimate from my own measurements on an N100 mini PC (13 October 2026), not a promise.</p>
<noscript><p class="sizer-noscript">This page needs JavaScript. Turn it on and reload the page.</p></noscript>
<section class="shell-section sizer" id="sizer" data-src="/assets/sizing-data.json?v=1" hidden>
<div class="sizer-presets" role="group" aria-label="Quick picks">
<button type="button" class="btn btn-outline btn-sm" data-preset="immich,jellyfin,nextcloud,home-assistant,vaultwarden,paperless-ngx">Family starter set</button>
<button type="button" class="btn btn-outline btn-sm" data-preset="nextcloud,vaultwarden">Just files and passwords</button>
<button type="button" class="btn btn-outline btn-sm" data-preset="">Clear</button>
</div>
<div class="sizer-layout">
<div>
<h2 class="sizer-sub">Applications</h2>
<div id="sizer-apps"></div>
</div>
<div class="sizer-result">
<h2>Estimate</h2>
<div id="sizer-result" aria-live="polite"></div>
</div>
</div>
</section>
<section class="shell-section sizer-advice-section">
<h2>What the machine needs</h2>
<ul class="sizer-advice">
<li>Processor: x86-64 (Intel or AMD), 4 cores, N100 class or newer.</li>
<li>System disk: a 256 GB SSD; 512 GB recommended.</li>
<li>Photos and films go on a separate data drive.</li>
<li>Not supported: Raspberry Pi and other ARM boards, Synology / QNAP, routers, rented cloud servers.</li>
</ul>
<details class="sizer-how">
<summary>How does the page count?</summary>
<p>The box's own services use about 470 MB of memory (Dashboard, tunnel, file manager, Docker). The page adds the measured figures of the apps you tick: once their typical use, once their peak, as if every app were busy at the same moment. That is a deliberately cautious count.</p>
<p>On an 8, 16 or 32 GB machine, the Felhom system gets the memory the installer gives it; the rest is for the base system. Fits well: one and a half times the peak still fits. Fits, but tight: the peak fits, one and a half times it does not. Does not fit: not even the peak fits.</p>
<p>The figures were measured on an N100 mini PC (13 October 2026): every app installed and at rest, the heavier ones also during one real task (a photo import, a film conversion, text recognition). A larger photo library or more users can need more.</p>
</details>
</section>
<script type="application/json" id="sizer-strings">{"typical": "Typical use", "peak": "At peak, if everything works at once", "platform_note": "Both include the box's own services (about 0.5 GB).", "guest": "Felhom gets {x} of this (the rest is for the base system).", "verdict": {"well": "Fits well", "tight": "Fits, but tight", "no": "Does not fit"}, "tight_why": "Works, but a large photo import or a backup at the same time can slow it down.", "cpu_head": "These make the processor work hard for a short time", "heavy_pair": "{names}: they slow each other down only while both are working.", "and": " and ", "unmeasured": "Not yet measured", "builtin": "built in, part of every box", "load_error": "The estimate's data did not load. Reload the page."}</script>
<footer>
<div class="container">
<p>© 2026 felhom.eu – Your own cloud, your own rules. Felhom serves households in Hungary.</p>
<p class="footer-legal">Operated by Nagyfenyvesi Viktor · <a href="mailto:info@felhom.eu">info@felhom.eu</a> · <a href="/adatkezeles">Privacy notice (Hungarian)</a> · <a href="/feltetelek">Terms (Hungarian)</a> · <a href="https://www.facebook.com/felhom.eu">Facebook</a></p>
</div>
</footer>
</main>
<script>
(function(){
var b=document.getElementById('navBurger'),s=document.getElementById('sidebar'),k=document.getElementById('navBackdrop');
if(!b||!s||!k)return;
function open(){s.classList.add('is-open');k.hidden=false;document.body.classList.add('nav-open');b.setAttribute('aria-expanded','true');}
function close(){s.classList.remove('is-open');k.hidden=true;document.body.classList.remove('nav-open');b.setAttribute('aria-expanded','false');}
b.addEventListener('click',function(){s.classList.contains('is-open')?close():open();});
k.addEventListener('click',close);
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&s.classList.contains('is-open')){close();b.focus();}});
s.querySelectorAll('a').forEach(function(l){l.addEventListener('click',close);});
window.addEventListener('resize',function(){if(window.innerWidth>768)close();});
})();
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var g=btn.closest('.nav-group');if(!g)return;
var willOpen=!g.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(x){
x.classList.remove('is-open');
var t=x.querySelector('.nav-group-toggle');if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){g.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
</script>
</body>
</html>
+143
View File
@@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<script>document.documentElement.className=document.documentElement.className.replace('no-js','js');</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Milyen gép kell? — becslés a kiválasztott alkalmazásokhoz | Felhom.eu</title>
<meta name="description" content="Jelöld be, milyen alkalmazásokat futtatnál, és megmutatja, hogy egy 8, 16 vagy 32 GB-os gépbe belefér-e. Becslés egy N100-as mini PC-n végzett mérésből.">
<meta name="robots" content="noindex">
<link rel="canonical" href="https://felhom.eu/gepigeny">
<link rel="alternate" hreflang="hu" href="https://felhom.eu/gepigeny">
<link rel="alternate" hreflang="en" href="https://felhom.eu/en/sizing">
<link rel="alternate" hreflang="x-default" href="https://felhom.eu/gepigeny">
<link rel="preload" as="font" type="font/woff2" crossorigin href="/assets/fonts/pjs-latin.woff2">
<link rel="preload" as="font" type="font/woff2" crossorigin href="/assets/fonts/pjs-latin-ext.woff2">
<!-- shared assets: bump ?v= together on any site.css/icons.svg change -->
<link rel="stylesheet" href="/assets/site.css?v=30">
<link rel="stylesheet" href="/assets/shell.css?v=25">
<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>
<link rel="stylesheet" href="/assets/sizing.css?v=1">
<script defer src="/assets/sizing.js?v=1"></script>
</head>
<body class="shell page-gepigeny">
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/assets/logo_notext_white.svg" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" id="navBurger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<span class="hamburger-box"><span class="hamburger-line"></span><span class="hamburger-line"></span><span class="hamburger-line"></span></span>
</button>
</header>
<div class="nav-backdrop" id="navBackdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<a href="/" class="sidebar-logo-link">
<img src="/assets/logo_notext_white.svg" alt="" class="sidebar-logo" aria-hidden="true">
<img src="/assets/logo-wordmark.png" alt="felhom.eu" class="sidebar-word">
</a>
</div>
<ul class="nav-links">
<li><a href="/"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-home"/></svg>Kezdőlap</a></li>
<li><a href="/szolgaltatasok"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-rocket"/></svg>Szolgáltatások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-apps"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg>Alkalmazások<svg class="ico nav-chevron" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-chevron-down"/></svg></button>
<ul id="nav-group-apps" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/alkalmazasok">Az alkalmazásokról</a></li>
<li><a href="/alkalmazasok#fajlkezeles">Fájlkezelés &amp; Irodai</a></li>
<li><a href="/alkalmazasok#media">Média &amp; Szórakozás</a></li>
<li><a href="/alkalmazasok#otthon">Otthon &amp; Életmód</a></li>
<li><a href="/alkalmazasok#produktivitas">Produktivitás &amp; Tudáskezelés</a></li>
<li><a href="/alkalmazasok#rendszer">Rendszer &amp; Fejlesztés</a></li>
</ul>
</li>
<li><a href="/technologiak"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-layers"/></svg>Technológiák</a></li>
<li><a href="/biztonsagimentes"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-shield-check"/></svg>Biztonsági mentés</a></li>
</ul>
<div class="sidebar-bottom">
<a href="/teszteles" class="btn btn-primary sidebar-cta">Jelentkezem tesztelőnek →</a>
<div class="nav-group-label">A Felhomról</div>
<ul class="nav-links nav-links-sub">
<li><a href="/#hogyan-mukodik">Hogyan működik</a></li>
<li><a href="/gyik">GYIK</a></li>
<li><a href="/kapcsolat">Kapcsolat</a></li>
</ul>
<div class="sidebar-footer">
<details class="lang-globe"><summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-globe"/></svg></summary><ul class="lang-globe-menu"><li><a href="/gepigeny" lang="hu" hreflang="hu" class="lang-globe-item current" aria-current="true">Magyar</a></li><li><a href="/en/sizing" lang="en" hreflang="en" class="lang-globe-item">English</a></li></ul></details>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<h1>Milyen gép <span>kell?</span></h1>
</div>
<p class="page-lede">Jelöld be, mit futtatnál a dobozon, és az oldal megmutatja, hogy egy 8, 16 vagy 32 GB memóriás gépbe hogyan fér bele. Ez becslés a saját mérésemből egy N100-as mini PC-n (2026. október 13.), nem ígéret.</p>
<noscript><p class="sizer-noscript">Ehhez az oldalhoz JavaScript kell. Kapcsold be, és töltsd újra az oldalt.</p></noscript>
<section class="shell-section sizer" id="sizer" data-src="/assets/sizing-data.json?v=1" hidden>
<div class="sizer-presets" role="group" aria-label="Gyors választás">
<button type="button" class="btn btn-outline btn-sm" data-preset="immich,jellyfin,nextcloud,home-assistant,vaultwarden,paperless-ngx">Családi alapcsomag</button>
<button type="button" class="btn btn-outline btn-sm" data-preset="nextcloud,vaultwarden">Csak fájlok és jelszavak</button>
<button type="button" class="btn btn-outline btn-sm" data-preset="">Törlés</button>
</div>
<div class="sizer-layout">
<div>
<h2 class="sizer-sub">Alkalmazások</h2>
<div id="sizer-apps"></div>
</div>
<div class="sizer-result">
<h2>Becslés</h2>
<div id="sizer-result" aria-live="polite"></div>
</div>
</div>
</section>
<section class="shell-section sizer-advice-section">
<h2>Amit a géptől várok</h2>
<ul class="sizer-advice">
<li>Processzor: x86-64 (Intel vagy AMD), 4 mag, N100 kategória vagy újabb.</li>
<li>Rendszerlemez: 256 GB SSD; ajánlott az 512 GB.</li>
<li>A fotók és a filmek külön adatlemezre kerüljenek.</li>
<li>Nem támogatott: Raspberry Pi és más ARM gépek, Synology / QNAP, routerek, bérelt felhőszerverek.</li>
</ul>
<details class="sizer-how">
<summary>Hogyan számol az oldal?</summary>
<p>A doboz saját szolgáltatásai kb. 470 MB memóriát használnak (vezérlőpult, alagút, fájlkezelő, Docker). Ehhez adja hozzá a bejelölt alkalmazások mért értékeit: egyszer az átlagos használatot, egyszer a csúcsot, mintha minden alkalmazás egyszerre dolgozna. Ez szándékosan óvatos számítás.</p>
<p>Egy 8, 16 vagy 32 GB-os gépen a Felhom-rendszer annyi memóriát kap, amennyit a telepítő ad neki; a többi az alaprendszeré. „Bőven elfér”: a csúcs másfélszerese is belefér. „Elfér, de szűkösen”: a csúcs belefér, a másfélszerese nem. „Nem fér el”: már a csúcs sem fér bele.</p>
<p>A számok egy N100-as mini PC-n mért értékek (2026. október 13.): minden alkalmazás telepítve, nyugalomban, a nehezebbek egy valódi feladat közben is (fotóimport, filmátalakítás, szövegfelismerés). Egy nagyobb fotótár vagy több felhasználó többet kérhet.</p>
</details>
</section>
<script type="application/json" id="sizer-strings">{"typical": "Átlagos használat", "peak": "Csúcson, ha minden egyszerre dolgozik", "platform_note": "Mindkettőben benne vannak a doboz saját szolgáltatásai (kb. 0,5 GB).", "guest": "Ebből a Felhom-rendszer kap: {x} (a többi az alaprendszeré).", "verdict": {"well": "Bőven elfér", "tight": "Elfér, de szűkösen", "no": "Nem fér el"}, "tight_why": "Működik, de egy nagy fotóimport vagy egy közben futó mentés lelassíthatja.", "cpu_head": "Rövid ideig erősen dolgoztatják a processzort", "heavy_pair": "{names}: csak akkor lassítják egymást, amikor éppen egyszerre dolgoznak.", "and": " és ", "unmeasured": "Még nem mértük", "builtin": "beépítve, minden dobozban benne van", "load_error": "A becslés adatai nem töltődtek be. Töltsd újra az oldalt."}</script>
<footer>
<div class="container">
<p>© 2026 felhom.eu – Saját felhőd, saját szabályaid</p>
<p class="footer-legal">Üzemeltető: Nagyfenyvesi Viktor · <a href="mailto:info@felhom.eu">info@felhom.eu</a> · <a href="/adatkezeles">Adatkezelési tájékoztató</a> · <a href="/feltetelek">Feltételek</a> · <a href="https://www.facebook.com/felhom.eu">Facebook</a></p>
</div>
</footer>
</main>
<script>
(function(){
var b=document.getElementById('navBurger'),s=document.getElementById('sidebar'),k=document.getElementById('navBackdrop');
if(!b||!s||!k)return;
function open(){s.classList.add('is-open');k.hidden=false;document.body.classList.add('nav-open');b.setAttribute('aria-expanded','true');}
function close(){s.classList.remove('is-open');k.hidden=true;document.body.classList.remove('nav-open');b.setAttribute('aria-expanded','false');}
b.addEventListener('click',function(){s.classList.contains('is-open')?close():open();});
k.addEventListener('click',close);
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&s.classList.contains('is-open')){close();b.focus();}});
s.querySelectorAll('a').forEach(function(l){l.addEventListener('click',close);});
window.addEventListener('resize',function(){if(window.innerWidth>768)close();});
})();
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var g=btn.closest('.nav-group');if(!g)return;
var willOpen=!g.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(x){
x.classList.remove('is-open');
var t=x.querySelector('.nav-group-toggle');if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){g.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
</script>
</body>
</html>
+3
View File
@@ -1,6 +1,9 @@
User-agent: *
Allow: /
Disallow: /szolgaltatasok-nonpublic
# The machine estimator is live but unlisted until the operator lists it (R-997).
Disallow: /gepigeny
Disallow: /en/sizing
# /api/ is the contact form's POST endpoint, not a page: a GET returns 404 and Search
# Console reported it as "Not found (404)" (seen 2026-10-10, first crawled 2026-04-29).
Disallow: /api/