Audit at 22:15 MESZ flagged the conventional sum (lignite + hard coal
+ gas + nuclear) as 19.1 GW in the widget vs 15.65 GW in reality. The
breakdown showed the gap was Energy-Charts' `Fossil gas` (6.9 GW)
which bundles natural gas + GuD + similar, while SMARD's `Erdgas`
filter 4069 (3.8 GW) tracks just natural gas the way the widget label
implies. SMARD is the canonical German grid feed; switching the
primary source resolves the discrepancy directly.
- New `lib/smard.ts` fetches the 11 relevant filter IDs in parallel:
conventional (1223/4070/4069/1224), wind (4067/1225), solar (1226),
hydro (1228), biomass (4066), other-conventional + pumped storage
(1227/4071, surfaced separately), load (410). Two-step protocol
(index.json → archive.json) handled inside; output is MW (MWh × 4)
aligned to one common slot timestamp so every value on screen comes
from the same point in time.
- Aggregator route uses SMARD for production + load, Energy-Charts
only for cross-border saldo + day-ahead prices (where SMARD has no
convenient single-call endpoint). `dataSource: "smard" |
"energy-charts"` flag travels in the response so the widget can
attribute the numbers and fall back gracefully when SMARD is down.
- StrommixBlock gets a "Werte prüfen" disclosure that lists every
component (wind onshore + offshore, solar, the four conventional
buckets, biomass, hydro, load) with its MW value and the slot
timestamp + data source. Fact-check users can verify the addition
themselves now without asking.
Verified live: SMARD @ 21:00 UTC = lignite 8484 + hard coal 3480 +
gas 3811 + nuclear 0 = 15.78 GW conventional. Matches the user's
ground-truth 15.65 GW within sampling noise.
Audit against ground truth at 22:15 lokal exposed three real bugs:
1. Day-ahead price was wrong (widget 125, real 153 €/MWh). The CMS'
`last_non_null` transform always picks the last cell of the 96-slot
array — typically a slot in the future, not the slot covering "now".
Bypass the CMS for prices: aggregator fetches energy-charts /price
directly, walks unix_seconds, and picks the largest start ≤ now.
`priceMeasuredAtUnix` is surfaced so the widget can show the slot
timestamp as a sanity check.
2. Cross-border saldo (cbpf) typically lags 1.5–2 h. Widget labelled
it as "jetzt" even when the upstream's last datapoint was 90 min
old. Now the cross-border timestamp travels through the aggregator
and the widget renders "Stand 20:30 Uhr · verzögert" when the gap
to the production snapshot exceeds 30 min.
3. Bucket label "Dunkelflaute" misuses the term — the formal
definition needs ≥24 h history, not a single threshold crossing.
Updated the strommix-default content entry to use "Sehr niedrige
Einspeisung" / "Very low feed-in" instead, and explained the
reasoning in a comment so editors don't re-introduce the misnomer.
Aggregator response shape gained `crossborderMeasuredAtUnix` +
`priceMeasuredAtUnix`; the widget renders both as small timestamps
under their values, plus a "verzögert" hint when cross-border data is
older than the 30-min tolerance window.
Cache TTLs on the price external collections lowered from 30 min to
15 min so the admin-side cache flips at the same cadence as the
day-ahead slots (since 30.09.2025 prices come in 15-min buckets).
Editor-controlled live-strommix card that surfaces what share of
electricity demand is currently covered by weather-dependent
renewables (Wind+Solar / Load), plus the conventional generation that
fills the gap, the cross-border saldo, and the day-ahead price (DE/FR
side-by-side). Status bucket + label + argument come from the CMS so
editors can iterate copy without a deploy.
- LiveStrommixBlockData type next to the other block-types.
- New `/api/strommix` aggregator route bundles four CMS external
collections (strommix_de, crossborder_de, price_de, price_fr) into a
single payload, computes the renewable + conventional sums and the
cross-border saldo (Σ flow_xx_gw, sign convention: + = export from
DE), surfaces stale-on-error flags from the CMS response headers.
Cache-control 5min so a single node only hits energy-charts once
per window.
- StrommixBlock.svelte renders the card: percentage, bucket label,
bucket argument (markdown), context row (conventional + saldo with
↑/↓ arrow), price row with optional FR comparison + negative-price
badge, footer with disclaimer + "Wie wird das berechnet?" link +
stale-data hint when the CMS served fallback. Re-polls every 5min
client-side; bucket thresholds + labels + arguments come from the
block instance so multiple placements can carry different copy.
- ContentRows.svelte dispatcher branch on `_type === "live_strommix"`.
Wired against `cms_content/_types/core/live_strommix.json5` (added in
the rustycms repo today). content_layout.row{1,2,3}Content already
allow the new block-type via the same commit on the CMS side.