v1.8.16: Finanzen-Tab — Festpreis vs. Spot-Vergleich

Täglicher Tarif-Tracking: kWh + EPEX-Ø-Preis werden ab jetzt täglich
gespeichert. Neuer Finanzen-Tab zeigt Balkendiagramm (Festpreis vs.
Spot hypothetisch), Summen-Karten und Empfehlung ob flexibler Tarif
sich lohnen würde.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
retr0
2026-05-05 13:29:55 +02:00
co-authored by Claude Sonnet 4.6
parent dfb42e6902
commit 2456f356b4
4 changed files with 253 additions and 2 deletions
+114 -1
View File
@@ -175,6 +175,7 @@
<main>
<div class="tabs">
<div class="tab active" onclick="switchTab('energy')">Energie</div>
<div class="tab" onclick="switchTab('finance')">Finanzen</div>
<div class="tab" onclick="switchTab('live')">Live-Daten</div>
<div class="tab" onclick="switchTab('inverters')">Geräte</div>
<div class="tab" onclick="switchTab('settings')">Einstellungen</div>
@@ -185,6 +186,11 @@
<div id="energy-content"><div class="no-data">Warte auf erste Messung…</div></div>
</div>
<!-- Finance Panel -->
<div class="panel" id="panel-finance">
<div id="finance-content"><div class="no-data">Lade…</div></div>
</div>
<!-- Live Panel -->
<div class="panel" id="panel-live">
<div id="live-content"><div class="no-data">Warte auf erste Messung...</div></div>
@@ -430,11 +436,12 @@ function showToast(msg, type) {
}
function switchTab(name) {
["energy","live","inverters","settings"].forEach((t, i) => {
["energy","finance","live","inverters","settings"].forEach((t, i) => {
document.querySelectorAll(".tab")[i].classList.toggle("active", t === name);
document.querySelectorAll(".panel")[i].classList.toggle("active", t === name);
});
if (name === "energy" || name === "live") startRefresh(); else stopRefresh();
if (name === "finance") loadFinance();
}
// ── Energy Dashboard ──────────────────────────────────────────
@@ -705,6 +712,112 @@ function renderEnergy(inverters, aggregates, period, spotData) {
</div>`;
}
// ── Finance Tab ───────────────────────────────────────────────
async function loadFinance() {
const el = document.getElementById("finance-content");
let data;
try { data = await fetchJSON(api("api/finance")); }
catch(e) { el.innerHTML = '<div class="no-data">Fehler beim Laden</div>'; return; }
const { days, fixed_total_eur, spot_total_eur, savings_eur, period_start, spot_days, total_days } = data;
if (!days || days.length === 0) {
el.innerHTML = `<div style="text-align:center;padding:40px;color:var(--text-dim)">
<div style="font-size:32px;margin-bottom:12px">📊</div>
<div style="font-weight:600;margin-bottom:6px">Noch keine Daten</div>
<div style="font-size:13px">Der Tracker sammelt ab morgen täglich Daten.<br>Abrechnungsjahr ab ${period_start}.</div>
</div>`;
return;
}
const C = { fixed: '#58a6ff', spot: '#f0883e', grid: '#30363d', green: '#4caf82', red: '#e05c5c' };
// ── Empfehlung ───────────────────────────────────────────────
let empfehlung = '';
if (spot_total_eur !== null && spot_days >= 7) {
const lohnt = savings_eur > 0;
const col = lohnt ? C.green : C.red;
const icon = lohnt ? '✓' : '✗';
empfehlung = `<div style="display:flex;align-items:center;justify-content:space-between;background:var(--surface);border:1px solid ${col};border-radius:var(--radius);padding:14px 18px;margin-bottom:16px">
<div>
<div style="font-weight:700;font-size:15px;color:${col}">${icon} Flexibler Tarif würde sich ${lohnt ? 'lohnen' : 'nicht lohnen'}</div>
<div style="font-size:12px;color:var(--text-dim);margin-top:3px">Basierend auf ${spot_days} Tagen im Abrechnungsjahr</div>
</div>
<div style="font-size:22px;font-weight:700;color:${col}">${lohnt ? '−' : '+'}${fEur(Math.abs(savings_eur))}</div>
</div>`;
}
// ── Summary-Karten ───────────────────────────────────────────
const spotCard = spot_total_eur !== null
? `<div class="kwh-card" style="border-top:3px solid ${C.spot}">
<div class="kv" style="color:${C.spot}">${fEur(spot_total_eur)}</div>
<div class="kl">Spot-Tarif (hypothetisch)<br><span style="opacity:.6">${spot_days} Tage mit Preisdaten</span></div>
</div>`
: `<div class="kwh-card"><div class="kv" style="color:var(--text-dim)">–</div><div class="kl">Spot-Tarif<br><span style="opacity:.6">Noch keine Daten</span></div></div>`;
const savCard = savings_eur !== null
? `<div class="kwh-card" style="border-top:3px solid ${savings_eur > 0 ? C.green : C.red}">
<div class="kv" style="color:${savings_eur > 0 ? C.green : C.red}">${savings_eur > 0 ? '−' : '+'}${fEur(Math.abs(savings_eur))}</div>
<div class="kl">${savings_eur > 0 ? 'Ersparnis mit Spot' : 'Mehrkosten mit Spot'}<br><span style="opacity:.6">gegenüber Festpreis</span></div>
</div>`
: '';
const cards = `<div class="energy-kwh" style="margin-bottom:20px">
<div class="kwh-card" style="border-top:3px solid ${C.fixed}">
<div class="kv" style="color:${C.fixed}">${fEur(fixed_total_eur)}</div>
<div class="kl">Festpreis-Kosten<br><span style="opacity:.6">${total_days} Tage</span></div>
</div>
${spotCard}${savCard}
</div>`;
// ── SVG-Balkendiagramm ────────────────────────────────────────
const W = 600, H = 180, PL = 44, PR = 12, PT = 10, PB = 28;
const cW = W - PL - PR;
const cH = H - PT - PB;
const n = days.length;
const barW = Math.max(2, Math.floor(cW / n) - 2);
const gap = Math.max(1, Math.floor(cW / n) - barW);
const allVals = days.flatMap(d => [d.fixed_eur, d.spot_eur].filter(v => v != null));
const maxVal = Math.max(...allVals, 0.01);
const yTicks = 4;
let gridLines = '', yLabels = '';
for (let i = 0; i <= yTicks; i++) {
const v = maxVal * i / yTicks;
const y = PT + cH - (cH * i / yTicks);
gridLines += `<line x1="${PL}" y1="${y.toFixed(1)}" x2="${W-PR}" y2="${y.toFixed(1)}" stroke="${C.grid}" stroke-width="0.5"/>`;
yLabels += `<text x="${PL-4}" y="${(y+4).toFixed(1)}" text-anchor="end" font-size="9" fill="#8b949e">${fEur(v)}</text>`;
}
let bars = '', xLabels = '';
days.forEach((d, i) => {
const x0 = PL + i * (barW + gap);
const hFixed = cH * d.fixed_eur / maxVal;
bars += `<rect x="${x0}" y="${(PT + cH - hFixed).toFixed(1)}" width="${barW/2}" height="${hFixed.toFixed(1)}" fill="${C.fixed}" opacity="0.85"/>`;
if (d.spot_eur != null) {
const hSpot = cH * d.spot_eur / maxVal;
bars += `<rect x="${x0 + barW/2}" y="${(PT + cH - hSpot).toFixed(1)}" width="${barW/2}" height="${hSpot.toFixed(1)}" fill="${C.spot}" opacity="0.85"/>`;
}
if (i % Math.max(1, Math.floor(n / 8)) === 0) {
const label = d.date.slice(5);
xLabels += `<text x="${(x0 + barW/2).toFixed(1)}" y="${H - 4}" text-anchor="middle" font-size="9" fill="#8b949e">${label}</text>`;
}
});
const legend = `<text x="${PL}" y="${H+18}" font-size="10" fill="${C.fixed}">■ Festpreis</text>
<text x="${PL+70}" y="${H+18}" font-size="10" fill="${C.spot}">■ Spot (hypothetisch)</text>`;
const chart = `<div style="margin-bottom:20px;overflow-x:auto">
<svg viewBox="0 0 ${W} ${H+24}" style="width:100%;max-width:${W}px;display:block">
${gridLines}${yLabels}${bars}${xLabels}${legend}
</svg>
</div>`;
el.innerHTML = empfehlung + cards + chart;
}
// ── Live Data ─────────────────────────────────────────────────
async function refreshData() {