feat(strommix): history-driven panels (residual load, dunkelflaute, neg-price, flow balance)
Aggregator route /api/strommix now reads three RustyCMS history-mode collections in parallel — strommix_history_de, price_de_history, crossborder_history_de — and projects derived metrics on top of the raw upstream snapshots stored there: - todayHourly: per-slot Last + Wind+Solar for the residual-load chart - negPriceHoursYtd: count of YTD slots where DE-LU day-ahead < 0 - crossborderTodayGwh / netCrossborderGwhYtd / crossborderByCountryGwhYtd: GW × slot-hours integrated into a per-country GWh balance - lastDunkelflaute / worstDunkelflauten: contiguous runs where (wind+solar)/load drops below 10 % for at least 1 h, top 5 by duration All four are null-graceful — collections that the CMS hasn't backfilled yet leave the corresponding panel hidden instead of breaking the widget. Five new components in lib/components/blocks/strommix/: - ResidualLoadChart.svelte — SVG line chart, no chart-lib dep - CapacityFactorBars.svelte — live vs installed Wind/Wind-Off/Solar - DunkelflauteCounter.svelte — last phase card + top-5 list - NegPriceCounter.svelte — YTD hours + manual EinsMan compensation - FlowBalanceTodayYtd.svelte — today + YTD saldo with per-country breakdown Block schema additions (block-types.ts + strommix-default.json5 in cms_content): installed_*_gw / installed_as_of for the capacity-factor bars (BNetzA Marktstammdatenregister snapshot, manually maintained), and einsman_costs_ytd_eur_mio / einsman_costs_as_of for the NegPrice panel (BNetzA quarterly report figures, no API).
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
/** Stunden mit negativem Day-Ahead-Preis YTD + manuell gepflegte
|
||||
* EinsMan-Entschädigungssumme aus dem Block. */
|
||||
type Props = {
|
||||
negHoursYtd: number | null;
|
||||
einsmanEurMio: number | null;
|
||||
einsmanAsOf: string | undefined;
|
||||
};
|
||||
|
||||
let { negHoursYtd, einsmanEurMio, einsmanAsOf }: Props = $props();
|
||||
|
||||
function fmtNum(n: number, fractionDigits = 0): string {
|
||||
return n.toLocaleString("de-DE", {
|
||||
minimumFractionDigits: fractionDigits,
|
||||
maximumFractionDigits: fractionDigits,
|
||||
});
|
||||
}
|
||||
|
||||
const showNeg = $derived(typeof negHoursYtd === "number");
|
||||
const showEinsman = $derived(typeof einsmanEurMio === "number");
|
||||
const visible = $derived(showNeg || showEinsman);
|
||||
|
||||
const yearLabel = $derived(new Date().getFullYear().toString());
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div class="flex flex-col gap-3">
|
||||
<header class="flex flex-col gap-0.5">
|
||||
<h4 class="text-sm font-semibold text-stein-800">
|
||||
Überschussstrom & Abregelung {yearLabel}
|
||||
</h4>
|
||||
<p class="m-0 text-[11px] text-stein-600">
|
||||
Hohe Wind- und Solarspitzen drücken den Strompreis, bei
|
||||
Netzengpässen werden Anlagen abgeregelt — und entschädigt.
|
||||
Beides Folgekosten der bisherigen Zubaupolitik.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
{#if showNeg && negHoursYtd != null}
|
||||
<div class="rounded-xs border-l-4 border-fire-400 bg-white px-3 py-2.5 shadow-sm">
|
||||
<div class="flex items-baseline gap-1.5">
|
||||
<span class="text-2xl font-bold tabular-nums text-stein-900">
|
||||
{fmtNum(negHoursYtd)}
|
||||
</span>
|
||||
<span class="text-xs text-stein-600">Stunden</span>
|
||||
</div>
|
||||
<span class="mt-0.5 block text-[11px] leading-snug text-stein-600">
|
||||
mit negativem Day-Ahead-Preis
|
||||
<span class="text-stein-400">(DE-LU)</span>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showEinsman && einsmanEurMio != null}
|
||||
<div class="rounded-xs border-l-4 border-fire-400 bg-white px-3 py-2.5 shadow-sm">
|
||||
<div class="flex items-baseline gap-1.5">
|
||||
<span class="text-2xl font-bold tabular-nums text-stein-900">
|
||||
{fmtNum(einsmanEurMio, 0)}
|
||||
</span>
|
||||
<span class="text-xs text-stein-600">Mio €</span>
|
||||
</div>
|
||||
<span class="mt-0.5 block text-[11px] leading-snug text-stein-600">
|
||||
Entschädigung Abregelung (§ 13 EnWG, § 51 EEG)
|
||||
{#if einsmanAsOf}
|
||||
<span class="block text-[10px] text-stein-400">
|
||||
Stand {einsmanAsOf}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user