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:
+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;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user