fix(strommix): pick price at "now" + surface cross-border timestamp
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).
This commit is contained in:
@@ -99,6 +99,15 @@
|
||||
return d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
/** Cross-border data is considered stale (and worth flagging) when its
|
||||
* timestamp is more than 30 min behind the production-snapshot. cbpf
|
||||
* typically lags 1.5–2 h, so this is the rule rather than the
|
||||
* exception — surfacing the stamp keeps the widget honest. */
|
||||
const cbStale = $derived.by(() => {
|
||||
if (!live?.measuredAtUnix || !live?.crossborderMeasuredAtUnix) return false;
|
||||
return live.measuredAtUnix - live.crossborderMeasuredAtUnix > 30 * 60;
|
||||
});
|
||||
|
||||
const bucketAccent = $derived.by(() => {
|
||||
if (!bucket) return "neutral";
|
||||
return bucket === "dunkelflaute"
|
||||
@@ -198,6 +207,7 @@
|
||||
<div class="px-4 py-3">
|
||||
<div class="text-xs text-neutral-600 uppercase tracking-wide">Konventionell jetzt</div>
|
||||
<div class="text-lg font-semibold tabular-nums">{fmtMw(live.conventionalMw)}</div>
|
||||
<div class="text-[11px] text-neutral-500 mt-0.5">Braunkohle + Steinkohle + Erdgas + Kernkraft</div>
|
||||
</div>
|
||||
<div class="px-4 py-3">
|
||||
<div class="text-xs text-neutral-600 uppercase tracking-wide">
|
||||
@@ -208,6 +218,12 @@
|
||||
{#if importDirection === "Export"}↑{/if}
|
||||
{fmtGw(live.netFlowGw)}
|
||||
</div>
|
||||
{#if live.crossborderMeasuredAtUnix}
|
||||
<div class="text-[11px] text-neutral-500 mt-0.5">
|
||||
Stand: {fmtTime(live.crossborderMeasuredAtUnix)} Uhr
|
||||
{#if cbStale}<span class="text-amber-700"> · verzögert</span>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -218,6 +234,11 @@
|
||||
{#if enableFr && live.priceFrEurMwh != null}
|
||||
<span class="text-neutral-600">(Frankreich: <span class="tabular-nums">{fmtPrice(live.priceFrEurMwh)}</span>)</span>
|
||||
{/if}
|
||||
{#if live.priceMeasuredAtUnix}
|
||||
<span class="text-[11px] text-neutral-500">
|
||||
· Slot {fmtTime(live.priceMeasuredAtUnix)}
|
||||
</span>
|
||||
{/if}
|
||||
{#if (live.priceDeEurMwh ?? 0) < 0}
|
||||
<span class="ml-auto inline-flex items-center gap-1 text-amber-800 font-semibold">
|
||||
<Icon icon="mdi:alert-circle-outline" class="size-4" aria-hidden="true" />
|
||||
|
||||
Reference in New Issue
Block a user