static-html-js-math-and-ux-fixes
File layout (single-file tool + PWA assets + harness):
cluster-estimator.html # the tool (all 4 fixes)
manifest.json # PWA manifest (fix #4)
sw.js # service worker precache (fix #4)
icon-192.png / icon-512.png # generated by gen_icons.py (fix #4)
gen_icons.py # pure-stdlib PNG generator
test.js # jsdom regression harness (4 groups / 13 checks)
gpusNeeded (main + pane)gpusNeeded is now Math.max(VRAM-fit ceil, tp * pp), so a TP=8 config can never report fewer than 8 GPUs. The same core path serves recalculate() and paneRecalculate():
function recalculate(kind) {
kind = kind || 'main';
var cfg = readConfig(kind);
var tp = Math.max(1, Math.floor(Number(cfg.tpSize) || 1)); // fix #3 belt-and-suspenders
var pp = Math.max(1, Math.floor(Number(cfg.ppSize) || 1));
var perGpu = Math.max(1, Number(cfg.perGpuVRAM) || 1);
var vramFit = Math.max(1, Math.ceil(estimateVRAMGB(cfg) / perGpu));
var gpusNeeded = Math.max(vramFit, tp * pp); // <-- TP×PP floor
last[kind] = { gpusNeeded, vramFit, tp, pp };
renderResults(kind, cfg, gpusNeeded, vramFit, tp, pp);
return gpusNeeded;
}
function paneRecalculate() { return recalculate('pane'); } // same floor, pane pane
lookupPrice returns a source; spec-list-price rows get an amber warning banner plus a per-row spec* marker in both the main and pane tables:
function lookupPrice(gpuKey) {
var cloud = CLOUD_PRICING[gpuKey];
if (cloud) return { price: cloud.price_hour, source: 'cloud' };
var spec = GPU_SPECS[gpuKey];
if (spec && Number.isFinite(spec.price_hour)) return { price: spec.price_hour, source: 'spec' };
return { price: null, source: 'none' };
}
// inside renderResults(kind, ...) — shared by main + pane:
var isFallback = hit.source === 'spec';
if (isFallback) anyFallback = true;
var priceCell = hit.price != null
? '$' + hit.price.toFixed(2) +
(isFallback ? '<span class="spec-price-marker" title="No cloud pricing — spec list price">spec*</span>' : '')
: '—';
...
var warn = document.getElementById('cloudFallbackWarn'); // shared banner
if (warn) warn.hidden = !anyFallback;
<div id="cloudFallbackWarn" class="amber-warn" hidden>
⚠ Some listed GPUs have no live cloud pricing — showing spec list price
(marked <span class="spec-price-marker">spec*</span>).
</div>
tpSize:0 renders no NaN/Infinity)setConfig/paneSetConfig clamp numerics: non-finite → min (or 1 if no min), sub-min → min, in-range values preserved verbatim. recalculate independently clamps TP/PP ≥ 1, so even a DOM-tampered input can't produce NaN. The key pitfall respected: never clamp to the input max attribute — the DeepSeek preset's headDim: 576 legitimately exceeds max="256", and presets are authoritative:
function clampNum(value, spec) {
var min = (typeof spec.min === 'number' && spec.min > 0) ? spec.min : 1;
var n = Number(value);
if (!Number.isFinite(n)) return min; // non-finite -> min-or-1
return Math.max(min, n); // up-to-min only; no max clamp
}
function setConfig(raw, kind) {
kind = kind || 'main';
var merged = clampConfig(Object.assign({}, readConfig(kind), raw || {}), kind);
writeConfig(merged, kind);
return merged;
}
gen_icons.py builds 192×192 and 512×512 RGBA PNGs with stdlib only (struct + zlib + CRC32), so no Pillow is required:
def _chunk(tag: bytes, data: bytes) -> bytes:
return (struct.pack(">I", len(data)) + tag + data
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
def _render(size: int) -> bytes:
raw = bytearray()
for y in range(size):
raw.append(0) # filter None
for x in range(size):
... # indigo→cyan gradient + white ring "C" monogram
raw += bytes((r, g, b, 255))
ihdr = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)
return (b"\x89PNG\r\n\x1a\n" + _chunk(b"IHDR", ihdr)
+ _chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + _chunk(b"IEND", b""))
manifest.json gains icons entries (192x192 + 512x512, any maskable) and sw.js precaches both icons plus the app shell:
const PRECACHE = ['./', './cluster-estimator.html', './manifest.json',
'./icon-192.png', './icon-512.png'];
Verified with the extended jsdom harness (`test.js`, run with Node 22 + jsdom): the new **5-check GAP-regression group** was added to the existing 3 groups — **4/4 harness groups PASS**:
```
[PASS] MATH: 2/2 # VRAM-fit math (8B→1 GPU; 700B→18 GPUs)
[PASS] TPxPP-FLOOR: 3/3 # TP=8→8; TP=4×PP=2→8; paneRecalculate TP=8→8
[PASS] CLAMP: 3/3 # tpSize:0→1; numParams:'abc'→0.1; pane NaN/-3→1
[PASS] GAP-REGRESSION: 5/5 # new group
ALL 13/13 checks PASS in 0.14s
4/4 harness groups PASS
```
GAP-regression checks: (1) TP=8 config never reports fewer than 8 GPUs; (2) amber `cloudFallbackWarn` visible; (3) per-row `spec-price-marker` present in **main and pane** tables; (4) imported JSON with `tpSize:0` renders **no NaN/Infinity**; (5) preset `headDim:576` (> input `max=256`) roundtrips losslessly in config **and** DOM.
**Edge battery (7/7 PASS):** DOM-tampered `tpSize='0'` then `recalculate()` → no NaN/Infinity, ≥1 GPU (recalc-level clamp works even bypassing `setConfig`); TP=8 summary text reads "8 GPUs"; pane import `tpSize:0` finite; RTX 4090 (spec-only) row shows `0.42` + `spec*` marker while H100 row uses cloud price with **no** marker; all `sw.js` precache files exist on disk.
**PWA assets:** `file` confirms valid `PNG image data, 192 x 192` / `512 x 512, 8-bit/color RGBA`; `manifest.json` parses as valid JSON; icons are `any maskable`.
**Pitfall confirmed live:** during harness bring-up, `getConfig()` without an argument returned `{}` (missing `kind || 'main'` default) — fixed; this is exactly the class of regression the roundtrip GAP-check guards.
---{"model": "deepseek-v4-flash", "problem_class": "static-html-js-math-and-ux-fixes", "result": "passed", "tests": 13}