diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index 8781ea97..61ee9a3a 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -121,7 +121,7 @@ stopping line that lies. | **R-516** | Install & onboarding | P3 | **[P3-LOW] English a customer meets on a fresh box and its apps' first screens — enumerated by the big night.** MEASURED 2026-09-14 (BIGNIGHT, VM 333, ISO 1.27.1, controller 0.242.0). Felhom-owned: (1) the dashboard menu item **„Debug"**; (2) the dashboard CPU tile **„Load: 0.29 / 0.39 / 0.37"**; (3) the launcher tile **„Filebrowser"** opens a login in English with no Felhom text (R-513); (4) the storage page mixes formal „Adjon hozzá / Csatlakoztasson" with the product's „te". App first screens a household meets before any Felhom text helps: (5) **Uptime Kuma 2.4 opens on „Which database would you like to use?"** (SQLite / Embedded MariaDB, „Next") — the app card's „Első lépések" does not mention it; (6) PrivateBin, Gokapi, AdventureLog and FileBrowser UIs are English (the apps' own). Already rows: the Proxmox installer screens (R-495, answered by the guide), `wiki.DOMAIN` (R-498). **Fix shape:** rename „Debug"/„Load" (controller); add the Uptime Kuma database step to its card, or pre-seed `db-config.json` for SQLite in the template (catalog). **Added by F4 (20:04:54Z):** (7) the storage page prints the disconnect time as a raw ISO UTC string „Leválasztva: 2026-09-14T19:58:02Z"; (8) the „Meghajtó leválasztva" banner appears twice on every page; (9) „4 telepített alkalmazás nem fut — nézze meg a rendszermonitort" uses the formal form. **Added by F7 (20:50–21:00Z, system disk at 95 %):** (10) a banner on every page in English, „**SSD disk usage high: 90%**"; (11) the dashboard tile reads „Rendszer (/) 61.8 GB / 68.7 GB (**90%**)" while `df` reports **95 %** (reserved blocks ignored), and „(/)" labels the data volume `/mnt/sys_drive`; the deploy page says nothing about free disk. **Added by the i18n spike (2026-09-17, controller v0.247.0):** (12) **six formal („ön") forms in the converted dashboard copy** — „Olvassa be telefonnal" (launcher QR hint), „Biztosan kikapcsolja a megosztást?" (launcher), „Ha újratelepíti … importálnia kell" (the layout's remove-app modal), „Kérjük, vegye fel a kapcsolatot" (backups empty state). They were NOT fixed — a localisation release may not change Hungarian bytes — and are now COUNTED by `controller/scripts/i18n_missing_gate.py` (`HU_FORMAL_CEILING` = 6, a ratchet: a new one convicts, fixing one lowers it). The inventory (`audits/I18N-INVENTORY-2026-09-17.md`) is the list this row closes against in localisation slice 6 (R-561). **Extended 2026-09-17 by slice 1 (controller v0.248.0–v0.250.0):** the converted copy now counts **16** formal forms (`HU_FORMAL_CEILING` 6 → 10 → 12 → 16, each raise stated, Hungarian unchanged by rule) — and the count is an UNDER-count: the gate's stem list sees 4 on the release C pages (storage, drive wizards, debug), while a wider list („írja be”, „adja meg”, „adjon hozzá”, „válassza ki”, „biztosan eltávolítja”, „engedélyezze”, „hozzon létre”, „kattintson”) finds at least **22** keys there alone (login „Adja meg a jelszavát”, the NAS guide, the storage confirms). Widen the stems when this row is worked; the ceiling rises with them. **NARROWED 2026-09-20 by localisation slice 6's walk, item by item** (`audits/i18n-slice6-2026-09-20/R-516-item-by-item.md`): item 2 FIXED (slice 1), items 3 and 6 APP-OWNED (and for an English household item 6 is an advantage), item 11 is a wrong NUMBER rather than a language and needs its own row, item 1 is correct English for an English reader and still open for a Hungarian one. **Items 4, 7, 8, 9 and 10 are NOT DECIDABLE FROM AN ENGLISH WALK** - they are about Hungarian copy quality, a disconnected drive and a full disk, none of which this walk had. **What this row is now waiting for is a HUNGARIAN walk on a box with a second drive**, not another English one; saying it closed would be the kind of closure that makes a register stop meaning anything. **-- 2026-10-08:** the recovery screen's own answers are Go literals in Hungarian only — 14 `renderRecovery` messages in `controller/internal/web/recovery_handlers.go` (seen: the safe default „A művelet nem fejeződött be…" rendered on an English page in a red-proof run). Today's new R-304 sentence is a bundle key in both languages; the 14 old ones are not. | **NARROWED 2026-09-20 - rank P3-LOW; owner: CC; needs a Hungarian walk** **2026-10-05 (burn-down night): items 1 and 4 FIXED on controller `main`** (`d7efa7b`): the menu says „Hibakeresés” (English „Debug”); the storage pages use the te-form (formal-form ceiling 17 → 14; 116 parity fixtures changed by exactly those bytes). Item 2 was already fixed. **Left:** items 5–6 (apps' own screens) and the rest. **2026-10-06: items 1 and 4 DELIVERED** (controller v0.298.0). **2026-10-06 (burn-down night, later): items 7–10 and 12 FIXED on controller `main`** (`e4774e6`, `2e5ef7e`): the disconnect time in local time; one banner per unplugged drive; „nézd meg”; the disk/memory/CPU/temperature banners in the household's language (`health.*` keys; the wire text to the hub unchanged and pinned); the last eleven counted formal forms are te-form (ceiling 14 → 0; 119 parity fixtures changed by exactly those bytes). **Left:** items 5–6 (apps' own screens); about 20 formal forms the gate does not count yet („írja be”, „adja meg”, „hozzon létre”, …) on the debug, storage, network-storage and security pages. Item 11 moved to its own row **R-889**. **2026-10-06 02:40: the bundle has NO formal form left** (controller `4c3c203`, unreleased): 49 sentences in the te-form; the gate's stem list widened (it counted 61 on the previous bundle, 0 now) with decoys and one listed third-person exception. **Left (narrowed):** Felhom-owned formal forms in Go and template literals the gate cannot read yet — the network-storage attach errors, escrow and share handlers, the single-copy notice, a settings refusal, the SMART mail, a fill-watch „Kérjük”, and the setup wizard pages; each moves into the bundle in the te-form. Items 5–6 are the apps' own screens (not ours). **2026-10-06: items 7–10, 12 and the bundle's 49 formal forms DELIVERED** in controller v0.299.0. **2026-10-06 night: the Go-literal leftover moved** (controller main `3d6ba28`, unreleased): 22 Go-literal messages — escrow handler (6), share password page (2), export upload (1), network-storage uid/remove/attach failures (13) — are bundle keys (`api.escrow.*`, `api.share.*`, `api.export.*`, `api.netstorage.*`) in the te-form with English (14 were formal); a detached attach failure renders in the reader's language; the NAS refusal says „Válassz". `TestR516_GoLiteralFormalFormsAreGone` (source scan + positive control), `TestR516_NetAddFailureFollowsTheReader`; red-proofs `audits/night-burndown-2026-10-06/ctrl/R-516-red*.txt`. **Left:** the setup wizard and recovery-info.txt (R-554); the SMART and fill-watch texts (the hub event's message — wire text, needs the household-copy producer shape); items 5–6 (apps' own screens). | — | — | CC | | **R-991** | Install & onboarding | P3 | **Nothing a buyer can read says which machines Felhom installs on, and a quarter of the forum beginners own only machines it cannot install on.** FOUND 2026-10-12 (forum research, `audits/forum-research-2026-10-12/FELHOM.md` §2). Searched `documentation/architecture/`, `website/` (`gyik`, `technologiak`, `letoltes`) and `scripts/felhom-host-install.sh`: no CPU-architecture, RAM, disk or network floor is written anywhere. The download page says only that the installer is built on Proxmox VE 9.2 — x86-64 only (inferred from Proxmox, not stated on the page). Of the 33 forum beginners who name what they own, 8 own only a Synology/QNAP/other NAS box, a router with a USB disk or a Raspberry Pi. The selfh.st survey reading reached the same gap independently (R-989, its recommendation 1). **Needs (operator):** the floor itself (RAM and disk minimum; whether thin clients and old laptops with 4–8 GB are accepted) — then one short section on the download page and in the FAQ. Not changed here: the research task forbade website edits. | **WAITING-ON-OPERATOR** — filed 2026-10-12 | the operator naming the floor | Operator: name the minimum (RAM, disk, x86-64); then CC writes the section on `letoltes.html` and `gyik.html` (both languages). If nothing is done: a buyer with a Pi or a Synology downloads an ISO that cannot run | operator | | **R-993** | Install & onboarding | P3 | **Operator: approve the minimum machine in `documentation/audits/sizing-2026-10-13/SIZING.md` §5** (the input R-991's hardware page needs). MEASURED 2026-10-13 on the N100 demo box (61 apps deployed and removed through the product, 15 min at rest each; heavy apps with one real action). Proposal: **minimum** x86-64, 4 cores (N100 class), **8 GB**, 256 GB SSD; **recommended 16 GB**, 512 GB SSD; out: ARM / Raspberry Pi, Synology-QNAP, routers, cloud VPS. Worked from the floor (2.5 GB for the whole box with no apps) + Immich, Jellyfin, Nextcloud, Home Assistant, Vaultwarden, Paperless-ngx at their peaks (5.0 GB) + 50 % headroom ≈ 12.3 GB. 8 GB runs that set without Immich; 16 GB runs all of it twice over. Data for the estimator: `sizing.json`. **2026-10-13 (estimator session) — the installer accepts a host BELOW this minimum:** `autosize_guest_caps` has no floor on the host, only on the guest; on a 4 GB box (MemTotal ≈ 3.5 GiB) the guest gets host − 1 GB ≈ 2.4 GB and Proxmox keeps ≈ 1 GB, where it used ≈ 2.0 GB on the N100 (SIZING.md §1). Whatever minimum is approved, nothing enforces it at install today. The three-verdict wording of §5 now matches the estimator page (`/gepigeny`, R-997). | **WAITING-ON-OPERATOR** — filed 2026-10-13 | the operator's decision | Operator: approve or change §5. If nothing is done: no hardware page; a buyer with a Raspberry Pi can still download the installer (R-991 stays open) | operator | -| **R-997** | Install & onboarding | P3 | **Operator: try the machine estimator at https://felhom.eu/gepigeny (English: /en/sizing) and say „list it” or what to change.** BUILT 2026-10-13: 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 which apps make the CPU work hard for a short time; numbers from the 2026-10-13 N100 measurement (`documentation/audits/sizing-2026-10-13/`). LIVE but UNLISTED: noindex, `Disallow` in robots.txt, not in the sitemap, linked from nowhere — `site_gates.py` gate 25 keeps it that way. It names no minimum machine (R-993) and gives no power or noise figure. Its JavaScript was never run by CC (no browser or JS engine on DooPlex): the operator's try is its first real run. **To list it** (one website task, after the power readings): move the pair from `UNLISTED` to `TWINS`, drop noindex and the two Disallow lines, add both URLs to the sitemap, link it from the menu and the FAQ. **v2 live 2026-10-13 afternoon:** fold-out categories and a memory bar (solid = fits well, thin mark = tight) with one line naming the box to buy; the reviewer's v1 browser check (Playwright: console clean, 390 px without sideways scroll, only felhom.eu + stats.felhom.eu) asked for the shorter page. | **WAITING-ON-OPERATOR** — filed 2026-10-13 | the operator's try | Operator: open the link, tick a few apps, then say „list it” or what to change. If nothing is done: the page stays unlisted and nobody finds it | operator | +| **R-997** | Install & onboarding | P3 | **Operator: try the machine estimator at https://felhom.eu/gepigeny (English: /en/sizing) and say „list it” or what to change.** BUILT 2026-10-13: 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 which apps make the CPU work hard for a short time; numbers from the 2026-10-13 N100 measurement (`documentation/audits/sizing-2026-10-13/`). LIVE but UNLISTED: noindex, `Disallow` in robots.txt, not in the sitemap, linked from nowhere — `site_gates.py` gate 25 keeps it that way. It names no minimum machine (R-993) and gives no power or noise figure. Its JavaScript was never run by CC (no browser or JS engine on DooPlex): the operator's try is its first real run. **To list it** (one website task, after the power readings): move the pair from `UNLISTED` to `TWINS`, drop noindex and the two Disallow lines, add both URLs to the sitemap, link it from the menu and the FAQ. **v2 live 2026-10-13 afternoon:** fold-out categories and a memory bar (solid = fits well, thin mark = tight) with one line naming the box to buy; the reviewer's v1 browser check (Playwright: console clean, 390 px without sideways scroll, only felhom.eu + stats.felhom.eu) asked for the shorter page. **v3 live 2026-10-13 evening:** the bar shows what the box memory is used for (base system, Felhom, each app, spare room) with a legend and one explaining sentence; „an 8 GB machine” fixed. The reviewer's v2 browser check: console clean, 1,677 px tall at 390 px (was 4,592), the bar sticky at 53 px and 124 px tall. | **WAITING-ON-OPERATOR** — filed 2026-10-13 | the operator's try | Operator: open the link, tick a few apps, then say „list it” or what to change. If nothing is done: the page stays unlisted and nobody finds it | operator | | **R-494** | Install & onboarding | P4 | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: operator-side automation; the operator creates the tunnel by hand per the day-0 runbook.** | — | — | CC | | **R-504** | Install & onboarding | P4 | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** **Re-ranked 2026-10-03: P3→P4: households are sent to the website's download page; the bare address is cosmetic.** | — | — | operator | diff --git a/scripts/CHANGELOG.md b/scripts/CHANGELOG.md index 0b93045c..dd8d0568 100644 --- a/scripts/CHANGELOG.md +++ b/scripts/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2026-10-13 (evening) — estimator v3: segments and the English article under test + +- `build_sizing_web.py`: `bar_app_colours` (8) in the data file, so the script holds no number of its own. +- `test_build_sizing_web.py` (10 tests): the bar's segments (base system = box − guest, Felhom, each app's peak, + spare = guest − peak) add up to the bar within 1 MB for the four cases and for nothing selected; base and spare are + never negative; the household preset by hand (spare = 5,515 × 0.5, base = 4,096 + 612); every English + recommendation gets „an 8" / „a 12" … Red-proofed: the „an 8" rule removed, the JS computing spare from the typical + figure, and segments off by 50 MB each convicted. + ## 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`. diff --git a/scripts/build_sizing_web.py b/scripts/build_sizing_web.py index 64bc5a9c..36cdd182 100644 --- a/scripts/build_sizing_web.py +++ b/scripts/build_sizing_web.py @@ -36,6 +36,7 @@ GUEST_RULE = {"reserve_mib": 4096, "max_gap_mib": 2048, "min_mib": 4096, "ceilin 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 +BAR_APP_COLOURS = 8 # v3: apps get their own colour up to this many; the rest join one segment BUILTIN = {"filebrowser"} # One short line per medium / heavy-in-bursts app (the 9 of 2026-10-13). sizing.json's English notes, tidied. @@ -119,6 +120,7 @@ def build(): boxes_gb=BOXES_GB, bar_marks_gb=BAR_MARKS_GB, bar_max_gb=BAR_MAX_GB, + bar_app_colours=BAR_APP_COLOURS, 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, ) diff --git a/scripts/test_build_sizing_web.py b/scripts/test_build_sizing_web.py index dd85088a..2663f904 100644 --- a/scripts/test_build_sizing_web.py +++ b/scripts/test_build_sizing_web.py @@ -59,6 +59,16 @@ def estimate(d, slugs): # mirrors sizing.js estimate() return peak, out +def segments(d, slugs): # mirrors estimate().segments in sizing.js (v3) + by = {a["slug"]: a for a in d["apps"]} + apps = sorted((by[s] for s in slugs if not by[s].get("builtin")), key=lambda a: -a["peak_mb"]) + peak = d["platform_mb"] + sum(a["peak_mb"] for a in apps) + well = min_box_gb(d, peak * d["headroom"]) + box, guest = well * 1024, guest_mib(d, well) + return well, [("base", box - guest), ("platform", d["platform_mb"])] + \ + [(a["slug"], a["peak_mb"]) for a in apps] + [("spare", guest - peak)] + + 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) @@ -112,6 +122,32 @@ class SizingWeb(unittest.TestCase): if rec is not None: self.assertGreaterEqual(rec, gb - 1 / 1024) + def test_segments_add_up_to_the_bar(self): + for name, sel in {"household": HOUSEHOLD, "household without immich": [x for x in HOUSEHOLD if x != "immich"], + "immich alone": ["immich"], "all 62": self.all, "nothing": []}.items(): + well, segs = segments(self.d, sel) + self.assertAlmostEqual(sum(mb for _, mb in segs), well * 1024, delta=1, msg=name) + self.assertGreaterEqual(dict(segs)["base"], 0, name + ": base system negative") + self.assertGreaterEqual(dict(segs)["spare"], 0, name + ": spare negative") + # the household preset, by hand: peak 5,515 MiB; the bar holds 5,515 * 1.5 = 8,272.5 in the guest + well, segs = segments(self.d, HOUSEHOLD) + self.assertAlmostEqual(dict(segs)["spare"], 5515 * 0.5, delta=1) + self.assertAlmostEqual(dict(segs)["base"], 4708, delta=1) # 4,096 for Proxmox + 612 MemTotal shortfall + + def test_english_article(self): + with io.open(os.path.join(ROOT, "website", "en", "sizing.html"), encoding="utf-8-sig") as f: + t = json.loads(re.search(r'id="sizer-strings">(.*?)', f.read(), re.S).group(1)) + def say(tpl, gb): # mirrors fill() in sizing.js + art = t["an"] if str(gb) in t["an_before"] else t["a"] + return tpl.replace("{a}", art).replace("{gb}", str(gb)) + got = {gb: say(t["recommend"], gb) for gb in self.d["boxes_gb"]} + self.assertEqual(got[8], "Recommended: an 8 GB machine.") + for gb in self.d["boxes_gb"]: + if gb != 8: + self.assertEqual(got[gb], "Recommended: a %d GB machine." % gb) + for k in ("tight_on", "only_tight", "none_fits"): + self.assertNotRegex(say(t[k], 8), r"\ba 8\b", k) + def test_sizing_md_table_agrees_with_the_rule(self): with io.open(SIZING_MD, encoding="utf-8") as f: s = f.read() @@ -146,7 +182,8 @@ class SizingWeb(unittest.TestCase): nums = set(re.findall(r"(?= needMib"): + for need in ("data.bar_max_gb", "data.bar_marks_gb", "function minBoxGb", "guestMib(data, mid) >= needMib", + "data.bar_app_colours", "boxMb - guestMb", "guestMb - peak"): self.assertIn(need, js) def test_data_has_no_peak_below_typical(self): diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index aee17f62..64131560 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -1,4 +1,21 @@ -## 2026-10-13 (afternoon) — machine estimator v2: fold-out categories and the memory bar +## 2026-10-13 (evening) — machine estimator v3: the bar shows what the box's memory is used for + +v2 said the apps use 5 GB at peak and then recommended 16 GB; the reason was invisible. Still unlisted (R-997). + +- **The bar is split** into the parts that make up its length (the „fits well" box memory, unchanged): the machine's + base system (grey, as the dashboard's „Rendszer"), Felhom's own services (accent blue), each selected app at its + peak (largest first, one colour each — the dashboard's „Memória eloszlás" palette without its amber and its + first blue; beyond 8 apps the smallest join „további N alkalmazás"), and spare room (striped). Every part has a + `title` with its name and MB; the thin „tight" mark stays; `role="meter"` keeps its value. +- **A legend** under the bar (not sticky): a dot, the name, the size in GB with one decimal, in that order, plus the + „tight from" mark. Only the bar and the recommendation line stay on screen while scrolling. +- **One explaining sentence** replaces the typical/peak line: the peak, the base system and the 50 % spare room — + three numbers from the rule — and why that makes the recommended size. The typical figure moved into + „Hogyan számol az oldal?". +- **English article:** „an 8 GB machine" (the article comes from the page's own strings). +- Cache versions `?v=3` for `sizing.css`, `sizing.js` and the data file. + +## 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). diff --git a/website/assets/sizing-data.json b/website/assets/sizing-data.json index ee2dfbd2..66d86cec 100644 --- a/website/assets/sizing-data.json +++ b/website/assets/sizing-data.json @@ -34,6 +34,7 @@ 64 ], "bar_max_gb": 64, + "bar_app_colours": 8, "categories": [ { "id": "fajlkezeles", diff --git a/website/assets/sizing.css b/website/assets/sizing.css index 47d4137b..e3963f0e 100644 --- a/website/assets/sizing.css +++ b/website/assets/sizing.css @@ -8,12 +8,32 @@ .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-stack { position: absolute; inset: 0; display: flex; overflow: hidden; border-radius: var(--radius); } +.sizer-seg { flex: 0 0 auto; height: 100%; 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; } + +/* v3: what the box's memory is used for — the controller's „Memória eloszlás” bar, same palette (monitoring.html), + minus its amber (amber is the warning colour here) and its first blue (Felhom's own services wear the accent) */ +.sizer { --sz-c1: #8E7CE8; --sz-c2: #5EC4B6; --sz-c3: #D67BB5; --sz-c4: #7FA6E8; + --sz-c5: #59B88C; --sz-c6: #E8935E; --sz-c7: #6FCADF; --sz-c8: #B3A1F0; } +.sizer-c1 { background: var(--sz-c1); } .sizer-c2 { background: var(--sz-c2); } .sizer-c3 { background: var(--sz-c3); } +.sizer-c4 { background: var(--sz-c4); } .sizer-c5 { background: var(--sz-c5); } .sizer-c6 { background: var(--sz-c6); } +.sizer-c7 { background: var(--sz-c7); } .sizer-c8 { background: var(--sz-c8); } +.sizer-seg-base { background: var(--text-3); opacity: 0.5; } +.sizer-seg-platform { background: var(--blue); } +.sizer-seg-more { background: var(--text-2); } +.sizer-seg-spare { background: repeating-linear-gradient(135deg, var(--line) 0 4px, var(--bg-2) 4px 8px); } +.sizer-seg-tightmark { background: var(--warn); } +.sizer-why-line { margin: 0 0 0.5rem; color: var(--text-2); line-height: 1.6; } +.sizer-why-line .sizer-num { font-family: var(--font-ui); } +.sizer-legend { list-style: none; margin: 0 0 1rem; padding: 0; display: flex; flex-wrap: wrap; gap: 0.3rem 1rem; font-size: 0.85rem; color: var(--text-2); } +.sizer-legend-item { display: inline-flex; align-items: center; gap: 0.4rem; } +.sizer-dot { width: 10px; height: 10px; border-radius: var(--radius); flex: 0 0 auto; } +.sizer-dot.sizer-seg-spare { border: 1px solid var(--line); } +.sizer-dot.sizer-seg-tightmark { width: 3px; height: 14px; } .sizer-result { margin: 0 0 1.25rem; } .sizer-totals { color: var(--text-2); margin: 0; line-height: 1.7; } diff --git a/website/assets/sizing.js b/website/assets/sizing.js index e12bcfbc..5d6f667c 100644 --- a/website/assets/sizing.js +++ b/website/assets/sizing.js @@ -11,7 +11,10 @@ * 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. + * holds peak * headroom (the bar's length) and peak (thin marker). It never restates the installer's rule. + * v3: the bar is split into what that box memory is used for, every part from the same rule: + * base system = box - guest(box) · Felhom = platform · each app = its peak · spare = guest(box) - peak + * so the parts add up to the box exactly (scripts/test_build_sizing_web.py proves it). * * The page text is not here: each page carries its own strings (#sizer-strings), so this file is the * same for both languages. @@ -64,10 +67,19 @@ if (tightBox === null && b.verdict === 'tight') { tightBox = b.gb; } }); var heavy = busy.filter(function (a) { return a.cpu_class === 'heavy-in-bursts'; }); + var wellGb = minBoxGb(data, peak * data.headroom); + var boxMb = wellGb * 1024, guestMb = guestMib(data, wellGb); + var apps = slugs.map(function (s) { return bySlug[s]; }) + .filter(function (a) { return a && !a.builtin; }) + .sort(function (x, y) { return y.peak_mb - x.peak_mb; }); + var segments = [{ kind: 'base', mb: boxMb - guestMb }, { kind: 'platform', mb: data.platform_mb }]; + apps.forEach(function (a) { segments.push({ kind: 'app', app: a, mb: a.peak_mb }); }); + segments.push({ kind: 'spare', mb: guestMb - peak }); 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 + well_gb: wellGb, tight_gb: minBoxGb(data, peak), + recommend_gb: recommend, tight_box_gb: tightBox, + segments: segments, base_mb: boxMb - guestMb, apps_peak_mb: peak - data.platform_mb }; } @@ -106,6 +118,13 @@ var meter = document.getElementById('sizer-meter'); var line = document.getElementById('sizer-line'); var result = document.getElementById('sizer-result'); + var stack = document.getElementById('sizer-stack'); + var legend = document.getElementById('sizer-legend'); + var why = document.getElementById('sizer-why-line'); + var typicalLine = document.getElementById('sizer-typical'); + // English needs "an 8 GB machine"; the article comes from the page's strings (none in Hungarian) + var article = function (gb) { return (T.an_before || []).indexOf(String(gb)) !== -1 ? T.an : T.a; }; + var fill = function (tpl, gb) { return tpl.replace('{a}', article(gb) || '').replace('{gb}', gb); }; fetch(host.getAttribute('data-src')).then(function (r) { return r.json(); }).then(function (data) { var max = data.bar_max_gb; @@ -160,11 +179,44 @@ 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 + '%'; + // the bar: its length is the "fits well" box memory, split into what that memory is used for + // (estimate().segments); beyond the colour count the smallest apps join one segment + var basis = Math.max(r.well_gb, max) * 1024; + var segs = [], rest = { kind: 'more', mb: 0, n: 0 }, nApps = 0; + r.segments.forEach(function (sg) { + if (sg.kind === 'app') { + nApps += 1; + if (nApps > data.bar_app_colours) { rest.mb += sg.mb; rest.n += 1; return; } + sg.colour = 'sizer-c' + nApps; + } + if (sg.kind === 'spare' && rest.n) { segs.push(rest); } + segs.push(sg); + }); + var label = function (sg) { + if (sg.kind === 'app') { return lang === 'en' ? sg.app.name_en : sg.app.name_hu; } + if (sg.kind === 'more') { return T.seg_more.replace('{n}', sg.n); } + return T['seg_' + sg.kind]; + }; + stack.textContent = ''; + legend.textContent = ''; + segs.forEach(function (sg) { + var cls = sg.kind === 'app' ? sg.colour : 'sizer-seg-' + sg.kind; + var part = el('div', 'sizer-seg ' + cls); + part.style.width = (sg.mb / basis * 100) + '%'; + part.title = label(sg) + ': ' + Math.round(sg.mb) + ' MB'; + stack.appendChild(part); + var li = el('li', 'sizer-legend-item'); + li.appendChild(el('span', 'sizer-dot ' + cls)); + li.appendChild(el('span', '', label(sg) + ' ')); + li.appendChild(el('span', 'sizer-num', gbText(sg.mb))); + legend.appendChild(li); + }); + var tli = el('li', 'sizer-legend-item'); + tli.appendChild(el('span', 'sizer-dot sizer-seg-tightmark')); + tli.appendChild(el('span', '', T.seg_tight + ' ')); + tli.appendChild(el('span', 'sizer-num', dec(r.tight_gb) + ' GB')); + legend.appendChild(tli); + document.getElementById('sizer-tight').style.left = (Math.min(r.tight_gb / max, 1) * 100) + '%'; 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))); @@ -173,23 +225,29 @@ // 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) : ''); + line.textContent = fill(T.recommend, r.recommend_gb) + + (r.tight_box_gb !== null ? ' ' + fill(T.tight_on, r.tight_box_gb) : ''); } else if (r.tight_box_gb !== null) { - line.textContent = T.only_tight.replace('{gb}', r.tight_box_gb); + line.textContent = fill(T.only_tight, r.tight_box_gb); } else { - line.textContent = T.none_fits.replace('{gb}', biggest); + line.textContent = fill(T.none_fits, 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 + ': ')); - 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)); + // the explaining sentence: three numbers, all from the rule; bold parts are {b}…{/b} + why.textContent = ''; + var tpl = r.apps_peak_mb > 0 + ? (r.recommend_gb !== null ? T.why : T.why_over) + : T.why_empty; + var spare = Math.round((data.headroom - 1) * 100); + tpl.replace('{peak}', dec(r.peak_mb / 1024)).replace('{base}', dec(r.base_mb / 1024)) + .replace('{base_platform}', dec((r.base_mb + data.platform_mb) / 1024)) + .replace('{spare}', spare).replace('{rec}', r.recommend_gb === null ? max : r.recommend_gb) + .split(/\{b\}|\{\/b\}/).forEach(function (part, i) { + why.appendChild(i % 2 ? el('strong', 'sizer-num', part) : document.createTextNode(part)); + }); + typicalLine.textContent = T.typical_line.replace('{typical}', gbText(r.typical_mb)).replace('{peak}', gbText(r.peak_mb)); if (r.tight_box_gb !== null) { result.appendChild(el('p', 'sizer-why', T.tight_why)); } if (r.busy.length) { diff --git a/website/en/sizing.html b/website/en/sizing.html index 1607bb8f..43b6b0b9 100644 --- a/website/en/sizing.html +++ b/website/en/sizing.html @@ -18,8 +18,8 @@ - - + +
@@ -73,7 +73,7 @@

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.

-