sddsf
All checks were successful
Deploy / deploy-staging (push) Successful in 5s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-05-04 22:38:51 +02:00
parent 748d2c2e59
commit 3795fc1de5
2 changed files with 182 additions and 49 deletions

View File

@@ -438,8 +438,29 @@
}
function SimpleChart(props) {
const series = Array.isArray(props.series)
? props.series
.map((item, index) => ({
key: item && item.key ? String(item.key) : `series-${index}`,
label: item && item.label ? String(item.label) : `Serie ${index + 1}`,
color: item && item.color ? String(item.color) : ['#60a5fa', '#2dd4bf', '#f59e0b', '#f472b6'][index % 4],
data: Array.isArray(item && item.data)
? item.data.filter((point) => point && point.y !== null && point.y !== undefined)
: [],
}))
.filter((item) => item.data.length > 0)
: [];
const points = Array.isArray(props.data) ? props.data.filter((point) => point && point.y !== null && point.y !== undefined) : [];
if (!points.length) {
const isMultiSeries = series.length > 0;
const activeSeries = isMultiSeries ? series : [{
key: 'default',
label: props.yLabel || 'Wert',
color: props.type === 'area' ? '#2dd4bf' : '#60a5fa',
data: points,
}];
const allPoints = activeSeries.flatMap((item) => item.data);
if (!allPoints.length) {
return h('div', { className: 'mc-empty' }, 'Keine Daten fuer diese Ansicht.');
}
@@ -463,21 +484,28 @@
const width = 640;
const height = 220;
const padding = 24;
const values = points.map((point) => Number(point.y));
const values = allPoints.map((point) => Number(point.y));
const minY = Math.min.apply(null, values);
const maxY = Math.max.apply(null, values);
const range = maxY - minY || 1;
const stepX = points.length > 1 ? (width - padding * 2) / (points.length - 1) : 0;
const coords = points.map((point, index) => {
const x = padding + stepX * index;
const y = height - padding - ((Number(point.y) - minY) / range) * (height - padding * 2);
return [x, y];
const seriesCoords = activeSeries.map((item) => {
const stepX = item.data.length > 1 ? (width - padding * 2) / (item.data.length - 1) : 0;
const coords = item.data.map((point, index) => {
const x = padding + stepX * index;
const y = height - padding - ((Number(point.y) - minY) / range) * (height - padding * 2);
return [x, y];
});
return {
...item,
coords,
line: coords.map((coord) => coord.join(',')).join(' '),
area: coords.length
? [[coords[0][0], height - padding]].concat(coords, [[coords[coords.length - 1][0], height - padding]])
.map((coord) => coord.join(',')).join(' ')
: '',
};
});
const line = coords.map((coord) => coord.join(',')).join(' ');
const area = coords.length
? [[coords[0][0], height - padding]].concat(coords, [[coords[coords.length - 1][0], height - padding]])
.map((coord) => coord.join(',')).join(' ')
: '';
return h('div', { className: 'mc-chart space-y-3' }, [
h('div', { key: 'meta', className: 'mc-flex-split mc-kicker' }, [
@@ -490,10 +518,9 @@
h('line', { key: 'mid', x1: padding, x2: width - padding, y1: height / 2, y2: height / 2 }),
h('line', { key: 'base', x1: padding, x2: width - padding, y1: height - padding, y2: height - padding }),
]),
props.type === 'area' ? h('polygon', { key: 'area', points: area, fill: 'rgba(45, 212, 191, 0.18)' }) : null,
props.type === 'bar'
? h('g', { key: 'bars' }, coords.map((coord, index) => {
const barWidth = Math.max(10, stepX * 0.6 || 24);
props.type === 'bar' && !isMultiSeries
? h('g', { key: 'bars' }, seriesCoords[0].coords.map((coord, index) => {
const barWidth = Math.max(10, (((width - padding * 2) / Math.max(seriesCoords[0].coords.length - 1, 1)) * 0.6) || 24);
return h('rect', {
key: index,
x: coord[0] - barWidth / 2,
@@ -504,28 +531,49 @@
fill: 'rgba(59, 130, 246, 0.75)',
});
}))
: h('polyline', {
key: 'line',
points: line,
fill: 'none',
stroke: props.type === 'area' ? '#2dd4bf' : '#60a5fa',
strokeWidth: 3,
strokeLinecap: 'round',
strokeLinejoin: 'round',
}),
h('g', { key: 'dots' }, coords.map((coord, index) => h('circle', {
key: index,
cx: coord[0],
cy: coord[1],
r: 4,
fill: '#f8fafc',
stroke: props.type === 'area' ? '#2dd4bf' : '#60a5fa',
strokeWidth: 2,
}))),
: h('g', { key: 'series' }, seriesCoords.flatMap((item, index) => {
const nodes = [];
if (props.type === 'area' && !isMultiSeries && item.area) {
nodes.push(h('polygon', {
key: `${item.key}-area`,
points: item.area,
fill: 'rgba(45, 212, 191, 0.18)',
}));
}
nodes.push(h('polyline', {
key: `${item.key}-line`,
points: item.line,
fill: 'none',
stroke: item.color,
strokeWidth: 3,
strokeLinecap: 'round',
strokeLinejoin: 'round',
}));
nodes.push(h('g', { key: `${item.key}-dots` }, item.coords.map((coord, pointIndex) => h('circle', {
key: `${item.key}-${pointIndex}`,
cx: coord[0],
cy: coord[1],
r: 4,
fill: '#f8fafc',
stroke: item.color,
strokeWidth: 2,
}))));
return nodes;
})),
]),
h('div', { key: 'labels', className: 'mc-mini-grid' },
points.slice(-3).map((point, index) => h('div', { key: index, className: 'mc-mini-card' }, `${point.x}: ${fmtNumber(point.y, 6)}`))
),
isMultiSeries
? h('div', { key: 'legend', className: 'mc-mini-grid' },
seriesCoords.map((item) => {
const latestPoint = item.data[item.data.length - 1] || null;
return h('div', { key: item.key, className: 'mc-mini-card' }, [
h('div', { key: 'label', className: 'mc-kicker', style: { color: item.color } }, item.label),
h('div', { key: 'value' }, latestPoint ? `${fmtNumber(latestPoint.y, 4)} · ${latestPoint.x}` : 'n/a'),
]);
})
)
: h('div', { key: 'labels', className: 'mc-mini-grid' },
points.slice(-3).map((point, index) => h('div', { key: index, className: 'mc-mini-card' }, `${point.x}: ${fmtNumber(point.y, 6)}`))
),
]);
}
@@ -1128,13 +1176,44 @@
loadSavedDashboards();
}, [payload, projectKey]);
const overviewCharts = useMemo(() => ({
mining: measurements.map((row) => ({ x: row.measured_at.slice(5, 16), y: row.coins_total })),
performance: measurements.filter((row) => row.doge_per_day_interval !== null)
.map((row) => ({ x: row.measured_at.slice(5, 16), y: row.doge_per_day_interval })),
pricing: measurements.filter((row) => row.price_per_coin !== null)
.map((row) => ({ x: row.measured_at.slice(5, 16), y: row.price_per_coin })),
}), [measurements]);
const overviewCharts = useMemo(() => {
const comparisonRows = measurements.filter((row) => {
const miningRate = Number(row.doge_per_hour_per_mh_interval);
const price = Number(row.effective_price_per_coin ?? row.price_per_coin);
return Number.isFinite(miningRate) && miningRate > 0 && Number.isFinite(price) && price > 0;
});
const baseComparison = comparisonRows[0] || null;
const baseMining = baseComparison ? Number(baseComparison.doge_per_hour_per_mh_interval) : null;
const basePrice = baseComparison ? Number(baseComparison.effective_price_per_coin ?? baseComparison.price_per_coin) : null;
return {
mining: measurements.map((row) => ({ x: row.measured_at.slice(5, 16), y: row.coins_total })),
performance: measurements.filter((row) => row.doge_per_day_interval !== null)
.map((row) => ({ x: row.measured_at.slice(5, 16), y: row.doge_per_day_interval })),
pricing: measurements.filter((row) => row.price_per_coin !== null)
.map((row) => ({ x: row.measured_at.slice(5, 16), y: row.price_per_coin })),
miningVsPrice: baseMining && basePrice ? [
{
key: 'mining-rate',
label: 'Mining/h je MH/s Index',
color: '#2dd4bf',
data: comparisonRows.map((row) => ({
x: row.measured_at.slice(5, 16),
y: (Number(row.doge_per_hour_per_mh_interval) / baseMining) * 100,
})),
},
{
key: 'doge-price',
label: 'DOGE-Kurs Index',
color: '#f59e0b',
data: comparisonRows.map((row) => ({
x: row.measured_at.slice(5, 16),
y: (Number(row.effective_price_per_coin ?? row.price_per_coin) / basePrice) * 100,
})),
},
] : [],
};
}, [measurements]);
async function submitMeasurement(fromPreview) {
const preview = normalizeOcrPreview(ocrPreview);
@@ -2139,6 +2218,10 @@
panel('Mining-Verlauf', 'Coins total der letzten 15 Tage.', h(SimpleChart, { type: 'line', data: overviewCharts.mining })),
panel('Performance-Verlauf', 'DOGE-pro-Tag-Raten der letzten 15 Tage.', h(SimpleChart, { type: 'area', data: overviewCharts.performance })),
panel('Kurs-Verlauf', 'Preiswerte der letzten 15 Tage.', h(SimpleChart, { type: 'line', data: overviewCharts.pricing })),
panel('Mining vs. Kurs', 'Index-Vergleich der letzten 15 Tage. Mining wird auf DOGE pro Stunde je MH/s normalisiert, beide Reihen starten bei 100.', h(SimpleChart, {
type: 'line',
series: overviewCharts.miningVsPrice,
})),
]),
panel('Zielmonitor', 'Rest-DOGE und Resttage werden gegen den letzten verfuegbaren Kurs je Zielwaehrung berechnet.',
h('div', { className: 'mc-target-grid' },