feat(configurations): rank by intent using CPU benchmark scores
Rework /configurations so the highlighted "best deals" actually reflect
what they claim. The old queries had two concrete bugs — the D1 path
pre-filtered to the top-100 cheapest and dedupeed per-category inside
that biased sample, while the DuckDB fallback used `DISTINCT ON (cpu)
ORDER BY seen DESC` which kept a near-random row per CPU instead of
the cheapest. Neither path used the Geekbench scores that phase 2-3 of
the CPU enrichment work already populated.
Consolidate both paths behind a shared category module and swap the
five component-focused categories for six intent-focused ones:
best price/performance, most affordable (with a GB5 multicore floor to
exclude Atom-class chips), best €/core, best €/GB RAM, best €/TB NVMe,
and best €/TB bulk storage (SATA + HDD combined). Ranking happens in
SQL with ROW_NUMBER() partitioned on (cpu, ram_size, is_ecc, storage
shape), so variants of the same CPU can coexist when they genuinely
win different categories. Per-query failures no longer crash the page
loader; the DuckDB fallback takes over cleanly. A "snapshot updated"
timestamp and an optional GPU deep-link round out the page.
Spec at specs/configurations-rework.md captures the full rationale,
decisions, and open follow-ups (per-CPU and per-config landing pages
for SEO).
feat(statistics): tabbed sections, honest gaps, smoothing overlay
Rework /statistics from one eight-chart scroll into four tabs behind a
sticky left rail that hangs in the page gutter from 2xl up. Only the
active panel mounts, and each card builds its canvas when it scrolls
into view.
Missing days are no longer drawn as straight lines. Every series is
aligned onto a shared daily grid derived from the days the crawler
actually observed: counts zero-fill (nobody listed a server that day),
prices break (there is no price to plot), and a collection outage breaks
every series at once.
Price charts gain a 7-day centered moving average over a receded raw
line, toggled from the rail. The average is null-preserving, so it never
bridges a gap. Chart.js bezier tension drops to 0 — the moving average
is the honest version of what tension was faking.
Series colours come from a fixed CVD-checked palette indexed by position
instead of Math.random() per render, which was repainting every chart on
each data update.
Fix four defects in the key metrics along the way:
- Per-unit price minima were set by fixed-price listings, whose
nvme_drives holds the configurator's *menu* rather than one machine's
drives (33 TB across eight entries, identical under two CPUs). One
such row decided the whole day. Excluding them moves the NVMe minimum
from €1.14/TB to €6.30/TB and NVMe-vs-HDD from 1.1x to 6.2x.
- The trend arrow never moved: FontAwesomeIcon does not swap its SVG
when the icon prop changes, so the tile kept the down arrow it mounted
with while showing a red "+4.08% rising".
- "Lowest price" was min(cheapest AMD, cheapest Intel), which can never
return a listing whose cpu_vendor is neither ("Intel®", "2x").
- The price index cannot exceed 1.0 by construction, so a subtitle
reading "Values > 1.0 = higher prices" described an unreachable state.
Drop the AMD-vs-Intel tile (it divided two unrelated machines) and
replace "ECC Premium" with an absolute best €/GB, since the old delta
measured RAM capacity, not error correction.
Also collapses the 37 per-datacenter queries into one grouped query per
country, and streams each result into state as it lands rather than in
three blocking batches.