Website: machine estimator v2 — fold-out categories and the memory bar (still unlisted, R-997)
gates / gates (push) Successful in 6m31s
gates / gates (push) Successful in 6m31s
Each category is a native <details>, closed at first, with an 'n selected' count; a preset opens what it ticks. A sticky memory bar (0-64 GB, marks 8/16/24/32/48/64+): solid fill = smallest box memory where the selection fits well, thin mark = where it fits, but tight; one line names the box to buy. minBoxGb() inverts guestMib() by bisection on it, so the rule is still written once. Box sizes 12/24/48/64 added; tests cover seven sizes and the inverse (red-proofed); SIZING.md table gains 24 GB.
This commit is contained in:
@@ -167,13 +167,13 @@ peak × 1.5 — which Immich alone does not meet on 8 GB ((470 + 3,061) × 1.5
|
||||
**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: generated from the rule; scripts/test_build_sizing_web.py checks this table (24 GB column added 2026-10-13, estimator v2) -->
|
||||
| Selection | Peak | 8 GB | 16 GB | 24 GB | 32 GB |
|
||||
|---|---|---|---|---|---|
|
||||
| Household set | 5.4 GB | does not fit | fits well | fits well | fits well |
|
||||
| Household set without Immich | 2.4 GB | fits well | fits well | fits well | fits well |
|
||||
| Immich alone | 3.4 GB | fits, but tight | fits well | fits well | fits well |
|
||||
| All 62 apps | 21.3 GB | does not fit | 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.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,12 @@
|
||||
## 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`.
|
||||
- `test_build_sizing_web.py` (8 tests): the four cases × seven sizes, with the recommended („fits well”) and the
|
||||
smallest „tight” size each; the inverse agrees with the forward rule (the guest at the answer holds the need, one
|
||||
step smaller does not); SIZING.md's table is read by its header and must carry the 24 GB column; `sizing.js` may
|
||||
hold only 0, 1, 2, 100 and 1024. Red-proofed: a wrong 24 GB cell, a missing 24 GB column, a hard-coded 64 in the
|
||||
JS, and an inverse 1 GB too big each convicted.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -33,7 +33,9 @@ HEADROOM = 1.5
|
||||
# 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]
|
||||
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.
|
||||
@@ -115,6 +117,8 @@ def build():
|
||||
headroom=HEADROOM,
|
||||
guest_rule=GUEST_RULE,
|
||||
boxes_gb=BOXES_GB,
|
||||
bar_marks_gb=BAR_MARKS_GB,
|
||||
bar_max_gb=BAR_MAX_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,
|
||||
)
|
||||
|
||||
@@ -36,6 +36,19 @@ def guest_mib(d, gb): # mirrors sizing.js guestMib()
|
||||
return mem
|
||||
|
||||
|
||||
def min_box_gb(d, need): # mirrors sizing.js minBoxGb(): a bisection on guest_mib, never a 2nd rule
|
||||
lo, hi = 0.0, 1.0
|
||||
while guest_mib(d, hi) < need:
|
||||
hi *= 2
|
||||
while hi - lo > 1 / 1024:
|
||||
mid = (lo + hi) / 2
|
||||
if guest_mib(d, mid) >= need:
|
||||
hi = mid
|
||||
else:
|
||||
lo = mid
|
||||
return hi
|
||||
|
||||
|
||||
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"))
|
||||
@@ -46,6 +59,10 @@ def estimate(d, slugs): # mirrors sizing.js estimate()
|
||||
return peak, out
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class SizingWeb(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -61,19 +78,39 @@ class SizingWeb(unittest.TestCase):
|
||||
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(self.d["boxes_gb"], [8, 12, 16, 24, 32, 48, 64])
|
||||
self.assertEqual([guest_mib(self.d, gb) for gb in self.d["boxes_gb"]],
|
||||
[4096, 7580, 11676, 19868, 28060, 44444, 60828])
|
||||
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):
|
||||
# columns: 8, 12, 16, 24, 32, 48, 64 GB; then the recommended ("fits well") and the smallest tight size
|
||||
W, T, N = "well", "tight", "no"
|
||||
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"]),
|
||||
"household": (HOUSEHOLD, [N, T, W, W, W, W, W], 16, 12),
|
||||
"household without immich": ([s for s in HOUSEHOLD if s != "immich"], [W, W, W, W, W, W, W], 8, None),
|
||||
"immich alone": (["immich"], [T, W, W, W, W, W, W], 12, 8),
|
||||
"all 62": (self.all, [N, N, N, N, T, W, W], 48, 32), # 24 GB: guest 19,868 < peak 21,783
|
||||
"nothing": ([], [W] * 7, 8, None),
|
||||
}
|
||||
for name, (sel, want) in cases.items():
|
||||
self.assertEqual(estimate(self.d, sel)[1], want, name)
|
||||
for name, (sel, want, rec, tight) in cases.items():
|
||||
peak, verdicts = estimate(self.d, sel)
|
||||
self.assertEqual(verdicts, want, name)
|
||||
self.assertEqual(first(self.d, verdicts, "well"), rec, name + " recommended")
|
||||
self.assertEqual(first(self.d, verdicts, "tight"), tight, name + " smallest tight")
|
||||
|
||||
def test_inverse_agrees_with_the_forward_rule(self):
|
||||
cases = [HOUSEHOLD, [s for s in HOUSEHOLD if s != "immich"], ["immich"], self.all, []]
|
||||
for sel in cases:
|
||||
peak, _ = estimate(self.d, sel)
|
||||
for need in (peak * self.d["headroom"], peak):
|
||||
gb = min_box_gb(self.d, need)
|
||||
self.assertGreaterEqual(guest_mib(self.d, gb), need) # it fits at the answer
|
||||
self.assertLess(guest_mib(self.d, gb - 2 / 1024), need) # and not one step smaller
|
||||
# the recommendation is the first listed size at or above the fill
|
||||
rec = next((b for b in self.d["boxes_gb"] if guest_mib(self.d, b) >= need), None)
|
||||
if rec is not None:
|
||||
self.assertGreaterEqual(rec, gb - 1 / 1024)
|
||||
|
||||
def test_sizing_md_table_agrees_with_the_rule(self):
|
||||
with io.open(SIZING_MD, encoding="utf-8") as f:
|
||||
@@ -83,7 +120,9 @@ class SizingWeb(unittest.TestCase):
|
||||
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", "---"):
|
||||
if cells and cells[0] == "Selection":
|
||||
cols = [int(c.split()[0]) for c in cells[2:]]
|
||||
elif len(cells) > 2 and not cells[0].startswith("---"):
|
||||
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}
|
||||
@@ -91,7 +130,9 @@ class SizingWeb(unittest.TestCase):
|
||||
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)
|
||||
by = dict(zip(self.d["boxes_gb"], verdicts))
|
||||
self.assertIn(24, cols, "SIZING.md's table lacks the 24 GB column")
|
||||
self.assertEqual(rows[name][2:], [WORDS[by[c]] for c in cols], name)
|
||||
|
||||
def test_js_has_the_rule_and_none_of_its_numbers(self):
|
||||
js = io.open(JS, encoding="utf-8").read()
|
||||
@@ -103,7 +144,10 @@ class SizingWeb(unittest.TestCase):
|
||||
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"}))
|
||||
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"):
|
||||
self.assertIn(need, js)
|
||||
|
||||
def test_data_has_no_peak_below_typical(self):
|
||||
for a in self.d["apps"]:
|
||||
|
||||
+17
-1
@@ -1,4 +1,20 @@
|
||||
## 2026-10-13 — the machine estimator, live but unlisted (`/gepigeny`, `/en/sizing`)
|
||||
## 2026-10-13 (afternoon) — machine estimator v2: fold-out categories and the memory bar
|
||||
|
||||
The page was 4,592 px tall on a phone (the reviewer's browser check of v1). Still unlisted (R-997).
|
||||
|
||||
- **Fold-out categories:** one native `<details>` per category, all closed at first, dressed like the FAQ's items;
|
||||
the summary shows „n kiválasztva” / „n selected”. A preset opens every category it ticks something in.
|
||||
- **The memory bar** above the list, sticky (under the phone's top bar): a 0–64 GB scale with marks at 8, 16, 24,
|
||||
32, 48 and 64+ GB. Solid fill = the smallest box memory where the selection fits well; a thin mark = where it fits,
|
||||
but tight. One line under it names the box to buy (the smallest of 8, 12, 16, 24, 32, 48, 64 GB that fits well)
|
||||
and the size where it is only tight. `role="meter"` with the needed GB as its value and text. A legend says which
|
||||
mark means what, so nothing is said by colour alone.
|
||||
- The three verdict cards are gone; the totals line, the „tight” explanation and the CPU lines stay under the bar.
|
||||
- **`sizing.js`:** `minBoxGb()` beside `guestMib()` — a bisection on `guestMib()` itself, so the installer's rule
|
||||
is still written once. Box sizes 12, 24, 48, 64 GB added in the data file (`boxes_gb`, `bar_marks_gb`, `bar_max_gb`).
|
||||
Cache versions: `sizing.css`, `sizing.js` and the data file `?v=2`.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -18,9 +18,22 @@
|
||||
},
|
||||
"boxes_gb": [
|
||||
8,
|
||||
12,
|
||||
16,
|
||||
32
|
||||
24,
|
||||
32,
|
||||
48,
|
||||
64
|
||||
],
|
||||
"bar_marks_gb": [
|
||||
8,
|
||||
16,
|
||||
24,
|
||||
32,
|
||||
48,
|
||||
64
|
||||
],
|
||||
"bar_max_gb": 64,
|
||||
"categories": [
|
||||
{
|
||||
"id": "fajlkezeles",
|
||||
|
||||
+42
-26
@@ -1,41 +1,57 @@
|
||||
/* 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. */
|
||||
so nothing here reaches the shell or another page. Tokens from site.css :root; no raw colours.
|
||||
v2 (2026-10-13): fold-out categories in the FAQ's look, and the memory bar (sticky on a phone). */
|
||||
.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-presets { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0 0 1rem; }
|
||||
|
||||
/* the memory bar — kept short: track, marks, one line, a legend (≈ 130 px, under a quarter of 390 x 800) */
|
||||
.sizer-sticky { position: sticky; top: 0; z-index: 5; background: var(--bg-0); border-bottom: 1px solid var(--line); padding: 0.75rem 0 0.6rem; margin: 0 0 1rem; }
|
||||
.sizer-meter { padding: 0 0 1.4rem; }
|
||||
.sizer-track { position: relative; height: 12px; background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); }
|
||||
.sizer-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0; background: var(--blue); border-radius: var(--radius); transition: width 0.25s ease; }
|
||||
.sizer-tightmark { position: absolute; top: -4px; bottom: -4px; width: 2px; margin-left: -1px; background: var(--warn); transition: left 0.25s ease; }
|
||||
.sizer-scale { position: relative; height: 1rem; margin-top: 0.3rem; }
|
||||
.sizer-mark { position: absolute; transform: translateX(-100%); padding-right: 2px; border-right: 1px solid var(--line); font-family: var(--font-data); font-size: 0.72rem; color: var(--text-3); white-space: nowrap; line-height: 1rem; }
|
||||
.sizer-line { margin: 0; color: var(--text-1); font-weight: 600; line-height: 1.45; }
|
||||
.sizer-legend { margin: 0.2rem 0 0; color: var(--text-3); font-size: 0.78rem; }
|
||||
|
||||
.sizer-result { margin: 0 0 1.25rem; }
|
||||
.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-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; }
|
||||
|
||||
/* fold-out categories: native <details>, dressed like the FAQ's items (gyik.html) */
|
||||
.sizer-cat { background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 12px; overflow: hidden; transition: border-color 0.3s ease; }
|
||||
.sizer-cat:hover { border-color: var(--blue); }
|
||||
.sizer-cat-head { list-style: none; display: flex; align-items: center; gap: 0.75rem; padding: 16px 20px; cursor: pointer; color: var(--text-1); font-weight: 600; transition: background 0.2s ease; }
|
||||
.sizer-cat-head::-webkit-details-marker { display: none; }
|
||||
.sizer-cat-head:hover { background: var(--blue-dim); }
|
||||
.sizer-cat-head:focus-visible { outline: 2px solid var(--blue-bright); outline-offset: -2px; }
|
||||
.sizer-cat-name { flex: 1 1 auto; min-width: 0; }
|
||||
.sizer-cat-count { flex: 0 0 auto; font-family: var(--font-data); font-size: 0.8rem; font-weight: 500; color: var(--blue-bright); }
|
||||
.sizer-chevron { flex: 0 0 auto; color: var(--blue-bright); transition: transform 0.3s ease; }
|
||||
.sizer-cat[open] .sizer-chevron { transform: rotate(180deg); }
|
||||
.sizer-cat-body { padding: 0 20px 14px; }
|
||||
.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; }
|
||||
|
||||
/* a phone: the shell's top bar is sticky at the top (≈ 53 px, shell.css), so the memory bar sits under it */
|
||||
@media (max-width: 768px) {
|
||||
.sizer-sticky { top: 53px; }
|
||||
.sizer-mark { font-size: 0.66rem; }
|
||||
}
|
||||
|
||||
+97
-34
@@ -1,15 +1,17 @@
|
||||
/* 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
|
||||
* THE RULE lives in guestMib(), minBoxGb() and estimate() 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
|
||||
* per box size: guest memory by the installer's rule (guestMib), then
|
||||
* well peak * headroom <= guest
|
||||
* tight peak <= guest < peak * headroom
|
||||
* no guest < peak
|
||||
* the memory bar: minBoxGb() inverts guestMib() by SEARCHING it — the smallest box memory whose guest
|
||||
* holds peak * headroom (solid fill) and peak (thin marker). It never restates the installer's rule.
|
||||
*
|
||||
* The page text is not here: each page carries its own strings (#sizer-strings), so this file is the
|
||||
* same for both languages.
|
||||
@@ -27,6 +29,18 @@
|
||||
return mem;
|
||||
}
|
||||
|
||||
// The smallest box memory (GB, not rounded to a product size) whose guest holds needMib. guestMib() never
|
||||
// decreases as the box grows, so a bisection on it is exact to 1 MiB; it uses the forward rule only.
|
||||
function minBoxGb(data, needMib) {
|
||||
var lo = 0, hi = 1;
|
||||
while (guestMib(data, hi) < needMib) { hi = hi * 2; }
|
||||
while (hi - lo > 1 / 1024) {
|
||||
var mid = (lo + hi) / 2;
|
||||
if (guestMib(data, mid) >= needMib) { hi = mid; } else { lo = mid; }
|
||||
}
|
||||
return hi;
|
||||
}
|
||||
|
||||
function estimate(data, slugs) {
|
||||
var bySlug = {};
|
||||
data.apps.forEach(function (a) { bySlug[a.slug] = a; });
|
||||
@@ -44,11 +58,20 @@
|
||||
var verdict = peak * data.headroom <= guest ? 'well' : (peak <= guest ? 'tight' : 'no');
|
||||
return { gb: gb, guest_mb: guest, verdict: verdict };
|
||||
});
|
||||
var recommend = null, tightBox = null;
|
||||
boxes.forEach(function (b) {
|
||||
if (recommend === null && b.verdict === 'well') { recommend = b.gb; }
|
||||
if (tightBox === null && b.verdict === 'tight') { tightBox = b.gb; }
|
||||
});
|
||||
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 };
|
||||
return {
|
||||
typical_mb: typical, peak_mb: peak, boxes: boxes, busy: busy, heavy: heavy, unmeasured: unmeasured,
|
||||
well_gb: minBoxGb(data, peak * data.headroom), tight_gb: minBoxGb(data, peak),
|
||||
recommend_gb: recommend, tight_box_gb: tightBox
|
||||
};
|
||||
}
|
||||
|
||||
root.FelhomSizing = { estimate: estimate, guestMib: guestMib };
|
||||
root.FelhomSizing = { estimate: estimate, guestMib: guestMib, minBoxGb: minBoxGb };
|
||||
|
||||
/* ---------------------------------------------------------------- the page (drawing only) */
|
||||
if (typeof document === 'undefined') { return; }
|
||||
@@ -60,10 +83,10 @@
|
||||
return e;
|
||||
}
|
||||
|
||||
function icon(name) {
|
||||
function icon(name, cls) {
|
||||
var ns = 'http://www.w3.org/2000/svg';
|
||||
var s = document.createElementNS(ns, 'svg');
|
||||
s.setAttribute('class', 'ico');
|
||||
s.setAttribute('class', cls || 'ico');
|
||||
s.setAttribute('aria-hidden', 'true');
|
||||
var u = document.createElementNS(ns, 'use');
|
||||
u.setAttribute('href', '/assets/icons.svg?v=1#' + name);
|
||||
@@ -77,17 +100,36 @@
|
||||
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 dec = function (v) { var s = v.toFixed(1); return lang === 'hu' ? s.replace('.', ',') : s; };
|
||||
var gbText = function (mb) { return dec(mb / 1024) + ' GB'; };
|
||||
var appsBox = document.getElementById('sizer-apps');
|
||||
var meter = document.getElementById('sizer-meter');
|
||||
var line = document.getElementById('sizer-line');
|
||||
var result = document.getElementById('sizer-result');
|
||||
|
||||
fetch(host.getAttribute('data-src')).then(function (r) { return r.json(); }).then(function (data) {
|
||||
var max = data.bar_max_gb;
|
||||
meter.setAttribute('aria-valuemax', String(max));
|
||||
var groups = [];
|
||||
|
||||
// the bar's marks: positions from the data, the last one reads "64+"
|
||||
var scale = document.getElementById('sizer-scale');
|
||||
data.bar_marks_gb.forEach(function (gb, i) {
|
||||
var m = el('span', 'sizer-mark', gb + (i === data.bar_marks_gb.length - 1 ? '+' : '') + ' GB');
|
||||
m.style.left = (gb / max * 100) + '%';
|
||||
scale.appendChild(m);
|
||||
});
|
||||
|
||||
// one fold-out per category, all closed at first
|
||||
data.categories.forEach(function (c) {
|
||||
var fs = el('fieldset', 'sizer-group');
|
||||
fs.appendChild(el('legend', '', lang === 'en' ? c.name_en : c.name_hu));
|
||||
var d = el('details', 'sizer-cat');
|
||||
var sum = el('summary', 'sizer-cat-head');
|
||||
sum.appendChild(el('span', 'sizer-cat-name', lang === 'en' ? c.name_en : c.name_hu));
|
||||
var count = el('span', 'sizer-cat-count');
|
||||
sum.appendChild(count);
|
||||
sum.appendChild(icon('i-chevron-down', 'ico sizer-chevron'));
|
||||
d.appendChild(sum);
|
||||
var body = el('div', 'sizer-cat-body');
|
||||
data.apps.filter(function (a) { return a.category === c.id; }).forEach(function (a) {
|
||||
var lab = el('label', 'sizer-app');
|
||||
var cb = el('input');
|
||||
@@ -95,13 +137,14 @@
|
||||
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);
|
||||
lab.appendChild(el('span', 'sizer-app-name', lang === 'en' ? a.name_en : a.name_hu));
|
||||
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);
|
||||
body.appendChild(lab);
|
||||
});
|
||||
appsBox.appendChild(fs);
|
||||
d.appendChild(body);
|
||||
appsBox.appendChild(d);
|
||||
groups.push({ details: d, count: count });
|
||||
});
|
||||
|
||||
function selected() {
|
||||
@@ -111,6 +154,34 @@
|
||||
|
||||
function draw() {
|
||||
var r = estimate(data, selected());
|
||||
|
||||
groups.forEach(function (g) {
|
||||
var n = g.details.querySelectorAll('input[type=checkbox]:checked:not(:disabled)').length;
|
||||
g.count.textContent = n ? T.selected.replace('{n}', n) : '';
|
||||
});
|
||||
|
||||
// the bar: solid fill = fits well, thin marker = fits, but tight
|
||||
var wellPct = Math.min(r.well_gb / max, 1) * 100;
|
||||
var tightPct = Math.min(r.tight_gb / max, 1) * 100;
|
||||
document.getElementById('sizer-fill').style.width = wellPct + '%';
|
||||
document.getElementById('sizer-tight').style.left = tightPct + '%';
|
||||
var over = r.well_gb > max;
|
||||
var needText = over ? T.over_max.replace('{gb}', max) : dec(r.well_gb) + ' GB';
|
||||
meter.setAttribute('aria-valuenow', String(Math.min(r.well_gb, max).toFixed(1)));
|
||||
meter.setAttribute('aria-valuetext', T.meter_text.replace('{x}', needText));
|
||||
|
||||
// one line: which box to buy, and where it is tight
|
||||
var biggest = data.boxes_gb[data.boxes_gb.length - 1];
|
||||
if (r.recommend_gb !== null) {
|
||||
line.textContent = T.recommend.replace('{gb}', r.recommend_gb) +
|
||||
(r.tight_box_gb !== null ? ' ' + T.tight_on.replace('{gb}', r.tight_box_gb) : '');
|
||||
} else if (r.tight_box_gb !== null) {
|
||||
line.textContent = T.only_tight.replace('{gb}', r.tight_box_gb);
|
||||
} else {
|
||||
line.textContent = T.none_fits.replace('{gb}', biggest);
|
||||
}
|
||||
|
||||
// under the bar: totals, why tight is tight, the CPU lines
|
||||
result.textContent = '';
|
||||
var totals = el('p', 'sizer-totals');
|
||||
totals.appendChild(el('span', '', T.typical + ': '));
|
||||
@@ -119,20 +190,7 @@
|
||||
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.tight_box_gb !== null) { result.appendChild(el('p', 'sizer-why', T.tight_why)); }
|
||||
|
||||
if (r.busy.length) {
|
||||
result.appendChild(el('h3', 'sizer-sub', T.cpu_head));
|
||||
@@ -161,8 +219,13 @@
|
||||
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;
|
||||
groups.forEach(function (g) {
|
||||
var any = false;
|
||||
Array.prototype.forEach.call(g.details.querySelectorAll('input[type=checkbox]:not(:disabled)'), function (c) {
|
||||
c.checked = want.indexOf(c.value) !== -1;
|
||||
if (c.checked) { any = true; }
|
||||
});
|
||||
if (any) { g.details.open = true; }
|
||||
});
|
||||
draw();
|
||||
});
|
||||
@@ -170,7 +233,7 @@
|
||||
host.hidden = false;
|
||||
draw();
|
||||
}).catch(function () {
|
||||
result.textContent = T.load_error;
|
||||
line.textContent = T.load_error;
|
||||
host.hidden = false;
|
||||
});
|
||||
}
|
||||
|
||||
+15
-14
@@ -18,8 +18,8 @@
|
||||
<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>
|
||||
<link rel="stylesheet" href="/assets/sizing.css?v=2">
|
||||
<script defer src="/assets/sizing.js?v=2"></script>
|
||||
</head>
|
||||
<body class="shell page-gepigeny">
|
||||
<header class="mobile-topbar">
|
||||
@@ -70,25 +70,26 @@
|
||||
<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>
|
||||
<p class="page-lede">Tick what you would run on the box, and the page shows how much machine memory it needs. 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>
|
||||
<section class="shell-section sizer" id="sizer" data-src="/assets/sizing-data.json?v=2" 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 class="sizer-sticky">
|
||||
<div class="sizer-meter" id="sizer-meter" role="meter" aria-label="Machine memory needed" aria-valuemin="0" aria-valuemax="64" aria-valuenow="0">
|
||||
<div class="sizer-track"><div class="sizer-fill" id="sizer-fill"></div><div class="sizer-tightmark" id="sizer-tight"></div></div>
|
||||
<div class="sizer-scale" id="sizer-scale" aria-hidden="true"></div>
|
||||
</div>
|
||||
<p class="sizer-line" id="sizer-line" aria-live="polite"></p>
|
||||
<p class="sizer-legend">Solid bar: fits well · Thin mark: fits, but tight</p>
|
||||
</div>
|
||||
<div class="sizer-result" id="sizer-result"></div>
|
||||
<h2 class="sizer-sub">Applications</h2>
|
||||
<div class="sizer-cats" id="sizer-apps"></div>
|
||||
</section>
|
||||
|
||||
<section class="shell-section sizer-advice-section">
|
||||
@@ -102,11 +103,11 @@
|
||||
<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>On a 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 — the solid bar shows this, and it is the recommended size. Fits, but tight: the peak fits, one and a half times it does not — the thin mark shows this. The recommendation is the smallest of 8, 12, 16, 24, 32, 48 and 64 GB where it fits well.</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>
|
||||
<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).", "selected": "{n} selected", "recommend": "Recommended: a {gb} GB machine.", "tight_on": "It fits on {gb} GB, but tight.", "only_tight": "None of the sizes fits well; from {gb} GB it fits, but tight.", "none_fits": "It does not fit any size, not even {gb} GB.", "over_max": "more than {gb} GB", "meter_text": "Machine memory needed: {x}", "tight_why": "Tight: it 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">
|
||||
|
||||
+15
-14
@@ -18,8 +18,8 @@
|
||||
<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>
|
||||
<link rel="stylesheet" href="/assets/sizing.css?v=2">
|
||||
<script defer src="/assets/sizing.js?v=2"></script>
|
||||
</head>
|
||||
<body class="shell page-gepigeny">
|
||||
<header class="mobile-topbar">
|
||||
@@ -70,25 +70,26 @@
|
||||
<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>
|
||||
<p class="page-lede">Jelöld be, mit futtatnál a dobozon, és az oldal megmutatja, mekkora memóriájú gép kell hozzá. 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>
|
||||
<section class="shell-section sizer" id="sizer" data-src="/assets/sizing-data.json?v=2" 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 class="sizer-sticky">
|
||||
<div class="sizer-meter" id="sizer-meter" role="meter" aria-label="Szükséges gépmemória" aria-valuemin="0" aria-valuemax="64" aria-valuenow="0">
|
||||
<div class="sizer-track"><div class="sizer-fill" id="sizer-fill"></div><div class="sizer-tightmark" id="sizer-tight"></div></div>
|
||||
<div class="sizer-scale" id="sizer-scale" aria-hidden="true"></div>
|
||||
</div>
|
||||
<p class="sizer-line" id="sizer-line" aria-live="polite"></p>
|
||||
<p class="sizer-legend">Teli sáv: bőven elfér · Vékony jel: elfér, de szűkösen</p>
|
||||
</div>
|
||||
<div class="sizer-result" id="sizer-result"></div>
|
||||
<h2 class="sizer-sub">Alkalmazások</h2>
|
||||
<div class="sizer-cats" id="sizer-apps"></div>
|
||||
</section>
|
||||
|
||||
<section class="shell-section sizer-advice-section">
|
||||
@@ -102,11 +103,11 @@
|
||||
<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>Egy 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 — ezt mutatja a teli sáv, és ez az ajánlott gépméret. „Elfér, de szűkösen”: a csúcs belefér, a másfélszerese nem — ezt mutatja a vékony jel. Az ajánlás a 8, 12, 16, 24, 32, 48 és 64 GB-os méretek közül a legkisebb, amelybe bőven elfér.</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>
|
||||
<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).", "selected": "{n} kiválasztva", "recommend": "Ajánlott: {gb} GB-os gép.", "tight_on": "{gb} GB-on szűkösen elfér.", "only_tight": "Bőven egyik méretbe sem fér; {gb} GB-tól szűkösen elfér.", "none_fits": "Egyik méretbe sem fér bele, {gb} GB-ba sem.", "over_max": "több mint {gb} GB", "meter_text": "Szükséges gépmemória: {x}", "tight_why": "Szűkösen: 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">
|
||||
|
||||
Reference in New Issue
Block a user