no message
This commit is contained in:
@@ -4,31 +4,92 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>PV_Forecast</title>
|
<title>PV_Forecast</title>
|
||||||
<script src="https://code.highcharts.com/highcharts.js"></script>
|
<script src="https://code.highcharts.com/highcharts.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body { font-family: sans-serif; margin: 0; padding: 12px; }
|
body {
|
||||||
#chart { height: 420px; width: 100%; }
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||||
.row { display:flex; gap:12px; align-items:center; margin-bottom:10px; flex-wrap:wrap; }
|
margin: 0;
|
||||||
.badge { padding:4px 8px; border-radius:10px; background:#eee; }
|
padding: 0;
|
||||||
button { padding:6px 10px; cursor:pointer; }
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Deutlich sichtbare Kopfzeile */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #1f2937;
|
||||||
|
border-bottom: 1px solid #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reload {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
#reload:hover { background: #1d4ed8; }
|
||||||
|
#reload:active { transform: translateY(1px); }
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart Area */
|
||||||
|
#chart {
|
||||||
|
height: 520px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 10px 0 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kleine Hilfe, falls Symcon dunklen Hintergrund setzt */
|
||||||
|
.chartWrap {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="row">
|
<div class="wrap">
|
||||||
<button id="reload">Neu laden</button>
|
<div class="header">
|
||||||
<span class="badge" id="meta">…</span>
|
<button id="reload">🔄 Neu laden</button>
|
||||||
|
<div class="meta" id="meta">Lade…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="chartWrap">
|
||||||
<div id="chart"></div>
|
<div id="chart"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const metaEl = document.getElementById("meta");
|
const metaEl = document.getElementById("meta");
|
||||||
|
|
||||||
window.onerror = (msg, url, line, col) => {
|
window.onerror = (msg, url, line, col) => {
|
||||||
metaEl.textContent = `JS-Fehler: ${msg} (${line}:${col})`;
|
metaEl.textContent = `JS-Fehler: ${msg} (${line}:${col})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const instanceId = Number("{{INSTANCE_ID}}");
|
const instanceId = Number("{{INSTANCE_ID}}");
|
||||||
if (!Number.isFinite(instanceId) || instanceId <= 0) {
|
if (!Number.isFinite(instanceId) || instanceId <= 0) {
|
||||||
metaEl.textContent = "Fehler: INSTANCE_ID nicht korrekt.";
|
metaEl.textContent = "Fehler: INSTANCE_ID nicht korrekt eingesetzt.";
|
||||||
throw new Error("Invalid INSTANCE_ID");
|
throw new Error("Invalid INSTANCE_ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +100,7 @@
|
|||||||
let chart;
|
let chart;
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
metaEl.textContent = "Lade Daten…";
|
metaEl.textContent = "⏳ Lade Daten…";
|
||||||
const r = await fetch(endpoint, { cache: "no-store" });
|
const r = await fetch(endpoint, { cache: "no-store" });
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
const text = await r.text().catch(() => "");
|
const text = await r.text().catch(() => "");
|
||||||
@@ -48,6 +109,10 @@
|
|||||||
return await r.json();
|
return await r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmtDT(ms) {
|
||||||
|
return new Date(ms).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
function render(data) {
|
function render(data) {
|
||||||
if (typeof Highcharts === "undefined") {
|
if (typeof Highcharts === "undefined") {
|
||||||
metaEl.textContent = "Fehler: Highcharts nicht geladen (CSP/CDN).";
|
metaEl.textContent = "Fehler: Highcharts nicht geladen (CSP/CDN).";
|
||||||
@@ -56,25 +121,101 @@
|
|||||||
|
|
||||||
const forecast = data?.series?.forecast ?? [];
|
const forecast = data?.series?.forecast ?? [];
|
||||||
const actual = data?.series?.actual ?? [];
|
const actual = data?.series?.actual ?? [];
|
||||||
|
|
||||||
const cachedAt = data?.meta?.forecast_cached_at
|
const cachedAt = data?.meta?.forecast_cached_at
|
||||||
? new Date(data.meta.forecast_cached_at * 1000).toLocaleString()
|
? new Date(data.meta.forecast_cached_at * 1000).toLocaleString()
|
||||||
: "unbekannt";
|
: "unbekannt";
|
||||||
|
|
||||||
metaEl.textContent = `OK | Cache: ${cachedAt} | Forecast: ${forecast.length} | Ist: ${actual.length}`;
|
const nowMs = data?.meta?.now ?? Date.now();
|
||||||
|
|
||||||
|
metaEl.textContent = `✅ OK | Cache: ${cachedAt} | Forecast: ${forecast.length} | Ist: ${actual.length}`;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
title: {
|
||||||
|
text: "PV: Erwartung vs. Tatsächlich (heute)",
|
||||||
|
style: { fontWeight: "700" }
|
||||||
|
},
|
||||||
|
|
||||||
|
chart: {
|
||||||
|
animation: false,
|
||||||
|
spacingTop: 18
|
||||||
|
},
|
||||||
|
|
||||||
|
xAxis: {
|
||||||
|
type: "datetime",
|
||||||
|
plotLines: [{
|
||||||
|
color: "#ef4444",
|
||||||
|
width: 2,
|
||||||
|
value: nowMs,
|
||||||
|
dashStyle: "Dash",
|
||||||
|
label: {
|
||||||
|
text: "JETZT",
|
||||||
|
align: "right",
|
||||||
|
style: { color: "#ef4444", fontWeight: "700" }
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
|
||||||
|
yAxis: {
|
||||||
|
title: {
|
||||||
|
text: "Leistung (kW)",
|
||||||
|
style: { fontWeight: "700" }
|
||||||
|
},
|
||||||
|
gridLineColor: "#e5e7eb"
|
||||||
|
},
|
||||||
|
|
||||||
|
legend: { enabled: true },
|
||||||
|
|
||||||
|
tooltip: {
|
||||||
|
shared: true,
|
||||||
|
formatter: function () {
|
||||||
|
let s = `<b>${Highcharts.dateFormat('%d.%m.%Y %H:%M', this.x)}</b><br/>`;
|
||||||
|
this.points.forEach(p => {
|
||||||
|
const val = (p.y === null || typeof p.y === "undefined")
|
||||||
|
? "–"
|
||||||
|
: `${p.y.toFixed(1)} kW`;
|
||||||
|
s += `<span style="color:${p.color}">●</span> ${p.series.name}: <b>${val}</b><br/>`;
|
||||||
|
});
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
plotOptions: {
|
||||||
|
series: {
|
||||||
|
animation: false,
|
||||||
|
turboThreshold: 0,
|
||||||
|
marker: { enabled: true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "Erwartet (Solcast)",
|
||||||
|
data: forecast,
|
||||||
|
color: "#38bdf8",
|
||||||
|
lineWidth: 4,
|
||||||
|
marker: { radius: 4 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tatsächlich (Archiv)",
|
||||||
|
data: actual,
|
||||||
|
color: "#7c3aed",
|
||||||
|
lineWidth: 4,
|
||||||
|
marker: { radius: 5, symbol: "diamond" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
credits: { enabled: false }
|
||||||
|
};
|
||||||
|
|
||||||
if (!chart) {
|
if (!chart) {
|
||||||
chart = Highcharts.chart("chart", {
|
chart = Highcharts.chart("chart", options);
|
||||||
title: { text: "PV: Erwartung vs. Tatsächlich (heute)" },
|
|
||||||
xAxis: { type: "datetime" },
|
|
||||||
yAxis: { title: { text: "Leistung (kW)" } },
|
|
||||||
tooltip: { shared: true, xDateFormat: "%d.%m.%Y %H:%M" },
|
|
||||||
legend: { enabled: true },
|
|
||||||
series: [
|
|
||||||
{ name: "Erwartet (Solcast)", data: forecast },
|
|
||||||
{ name: "Tatsächlich (Archiv)", data: actual }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
|
// PlotLine "JETZT" aktualisieren + Daten neu setzen
|
||||||
|
chart.update({
|
||||||
|
xAxis: { plotLines: options.xAxis.plotLines }
|
||||||
|
}, false);
|
||||||
|
|
||||||
chart.series[0].setData(forecast, false);
|
chart.series[0].setData(forecast, false);
|
||||||
chart.series[1].setData(actual, false);
|
chart.series[1].setData(actual, false);
|
||||||
chart.redraw();
|
chart.redraw();
|
||||||
@@ -86,7 +227,7 @@
|
|||||||
const data = await loadData();
|
const data = await loadData();
|
||||||
render(data);
|
render(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
metaEl.textContent = "Fehler beim Laden: " + (e?.message ?? e);
|
metaEl.textContent = "❌ Fehler beim Laden: " + (e?.message ?? e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user