88514b065a
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).
130 lines
4.1 KiB
Svelte
130 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* "Live vs. installiert"-Bargraph für Wind onshore, Wind offshore und
|
|
* Solar. Kernaussage: das Schild auf dem Mast (installierte Leistung)
|
|
* ist nicht das, was zu jedem Zeitpunkt rauskommt.
|
|
*/
|
|
type Props = {
|
|
windOnshoreLiveMw: number;
|
|
windOffshoreLiveMw: number;
|
|
solarLiveMw: number;
|
|
installedWindOnshoreGw: number;
|
|
installedWindOffshoreGw: number;
|
|
installedSolarGw: number;
|
|
installedAsOf?: string;
|
|
};
|
|
|
|
let {
|
|
windOnshoreLiveMw,
|
|
windOffshoreLiveMw,
|
|
solarLiveMw,
|
|
installedWindOnshoreGw,
|
|
installedWindOffshoreGw,
|
|
installedSolarGw,
|
|
installedAsOf,
|
|
}: Props = $props();
|
|
|
|
type Row = {
|
|
label: string;
|
|
liveMw: number;
|
|
installedGw: number;
|
|
fillClass: string;
|
|
};
|
|
|
|
const rows: Row[] = $derived([
|
|
{
|
|
label: "Wind onshore",
|
|
liveMw: windOnshoreLiveMw,
|
|
installedGw: installedWindOnshoreGw,
|
|
fillClass: "bg-himmel-500",
|
|
},
|
|
{
|
|
label: "Wind offshore",
|
|
liveMw: windOffshoreLiveMw,
|
|
installedGw: installedWindOffshoreGw,
|
|
fillClass: "bg-himmel-700",
|
|
},
|
|
{
|
|
label: "Solar",
|
|
liveMw: solarLiveMw,
|
|
installedGw: installedSolarGw,
|
|
fillClass: "bg-erde-400",
|
|
},
|
|
]);
|
|
|
|
function pct(liveMw: number, installedGw: number): number {
|
|
if (installedGw <= 0) return 0;
|
|
return (liveMw / (installedGw * 1000)) * 100;
|
|
}
|
|
|
|
function fmtPct(p: number): string {
|
|
return (
|
|
p.toLocaleString("de-DE", {
|
|
maximumFractionDigits: p < 10 ? 1 : 0,
|
|
}) + " %"
|
|
);
|
|
}
|
|
|
|
function fmtGw(mw: number): string {
|
|
return (
|
|
(mw / 1000).toLocaleString("de-DE", {
|
|
maximumFractionDigits: 1,
|
|
}) + " GW"
|
|
);
|
|
}
|
|
</script>
|
|
|
|
<div class="flex flex-col gap-3">
|
|
<header class="flex flex-col gap-0.5">
|
|
<h4 class="text-sm font-semibold text-stein-800">
|
|
Auslastung jetzt vs. installiert
|
|
</h4>
|
|
{#if installedAsOf}
|
|
<span class="text-[11px] text-stein-500">
|
|
Installierte Leistung: Stand {installedAsOf},
|
|
Marktstammdatenregister
|
|
</span>
|
|
{/if}
|
|
</header>
|
|
|
|
<ul class="m-0! flex list-none flex-col gap-2.5 p-0">
|
|
{#each rows as row (row.label)}
|
|
{@const p = pct(row.liveMw, row.installedGw)}
|
|
<li class="flex flex-col gap-1">
|
|
<div class="flex justify-between text-xs">
|
|
<span class="font-medium text-stein-700">{row.label}</span>
|
|
<span class="tabular-nums text-stein-700">
|
|
{fmtGw(row.liveMw)}
|
|
<span class="text-stein-500">
|
|
/ {row.installedGw.toLocaleString("de-DE", {
|
|
maximumFractionDigits: 1,
|
|
})} GW
|
|
</span>
|
|
</span>
|
|
</div>
|
|
<div
|
|
class="relative h-2 overflow-hidden rounded-full bg-stein-200"
|
|
role="progressbar"
|
|
aria-valuemin="0"
|
|
aria-valuemax="100"
|
|
aria-valuenow={Math.round(p)}
|
|
>
|
|
<div
|
|
class="h-full rounded-full transition-[width] duration-500 {row.fillClass}"
|
|
style="width: {Math.min(100, p)}%"
|
|
></div>
|
|
</div>
|
|
<div class="flex justify-between text-xs tabular-nums">
|
|
<span class="font-semibold text-stein-800">{fmtPct(p)}</span
|
|
>
|
|
<span class="text-stein-500">
|
|
{Math.max(0, 100 - p).toLocaleString("de-DE", {
|
|
maximumFractionDigits: 0,
|
|
})} % nicht abgerufen
|
|
</span>
|
|
</div>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|