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

This commit is contained in:
2026-05-20 00:32:18 +02:00
parent 3ed4fba58c
commit 81d1e486e8
3 changed files with 75 additions and 28 deletions

View File

@@ -131,6 +131,38 @@
}).format(Number(value));
}
function recentMeasurementWindow(rows, windowDays) {
if (!Array.isArray(rows) || rows.length === 0) {
return [];
}
const normalizedWindowDays = Number(windowDays);
if (!Number.isFinite(normalizedWindowDays) || normalizedWindowDays <= 0) {
return rows;
}
const sortedRows = rows
.filter((row) => row && row.measured_at)
.slice()
.sort((left, right) => new Date(left.measured_at).getTime() - new Date(right.measured_at).getTime());
if (sortedRows.length === 0) {
return rows;
}
const latestTs = new Date(sortedRows[sortedRows.length - 1].measured_at).getTime();
if (!Number.isFinite(latestTs)) {
return sortedRows;
}
const minTs = latestTs - (normalizedWindowDays * 86400 * 1000);
const filteredRows = sortedRows.filter((row) => {
const measuredTs = new Date(row.measured_at).getTime();
return Number.isFinite(measuredTs) && measuredTs >= minTs;
});
return filteredRows.length ? filteredRows : [sortedRows[sortedRows.length - 1]];
}
function parseStoredUtcDate(value) {
const raw = String(value || '').trim();
if (!raw) {
@@ -1258,8 +1290,10 @@
}, [payload, projectKey]);
const overviewCharts = useMemo(() => {
const overviewWindowDays = Number(payload?.bootstrap_meta?.overview_window_days || 15);
const overviewRows = recentMeasurementWindow(measurements, overviewWindowDays);
const chartCoinCurrency = String(currentSettings.crypto_currency || 'DOGE').toUpperCase();
const comparisonRows = measurements.filter((row) => {
const comparisonRows = overviewRows.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;
@@ -1269,10 +1303,10 @@
const basePrice = baseComparison ? Number(baseComparison.effective_price_per_coin ?? baseComparison.price_per_coin) : null;
return {
mining: measurements.map((row) => ({ x: fmtDate(row.measured_at), y: row.coins_total })),
performance: measurements.filter((row) => row.doge_per_day_interval !== null)
mining: overviewRows.map((row) => ({ x: fmtDate(row.measured_at), y: row.coins_total })),
performance: overviewRows.filter((row) => row.doge_per_day_interval !== null)
.map((row) => ({ x: fmtDate(row.measured_at), y: row.doge_per_day_interval })),
pricing: measurements.filter((row) => row.price_per_coin !== null)
pricing: overviewRows.filter((row) => row.price_per_coin !== null)
.map((row) => ({ x: fmtDate(row.measured_at), y: row.price_per_coin })),
miningVsPrice: baseMining && basePrice ? [
{
@@ -1295,7 +1329,7 @@
},
] : [],
};
}, [currentSettings.crypto_currency, measurements]);
}, [currentSettings.crypto_currency, measurements, payload?.bootstrap_meta?.overview_window_days]);
async function submitMeasurement(fromPreview) {
const preview = normalizeOcrPreview(ocrPreview);

View File

@@ -1285,7 +1285,10 @@ final class Router
];
}
return $this->analytics()->buildSummary($measurements, $settings, $targets);
return $this->analytics()->buildSummary($measurements, $settings, $targets, [
'include_offer_scenarios' => $view === 'mining',
'include_long_term_projection' => $view === 'mining',
]);
}
private function filterMeasurementsToRecentWindow(array $rows, int $windowDays): array

View File

@@ -201,8 +201,11 @@ final class AnalyticsService
return $result;
}
public function buildSummary(array $measurements, array $settings, array $targets): array
public function buildSummary(array $measurements, array $settings, array $targets, array $options = []): array
{
$includeOfferScenarios = !array_key_exists('include_offer_scenarios', $options) || (bool) $options['include_offer_scenarios'];
$includeLongTermProjection = !array_key_exists('include_long_term_projection', $options) || (bool) $options['include_long_term_projection'];
if ($measurements === []) {
return [
'latest_measurement' => null,
@@ -355,28 +358,35 @@ final class AnalyticsService
'break_even_days_overall' => $this->roundOrNull($breakEvenDaysOverall, 4),
'break_even_eta_at' => $breakEvenProjection['eta'] ?? null,
]);
$currentProjection = $this->projectPerformance(
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
$purchasedMiners,
$latestSummary,
730
);
$latestSummary = array_merge($latestSummary, [
'projection_days' => $currentProjection['days'],
'projection_two_year_revenue' => $this->roundOrNull($currentProjection['revenue'], 8),
'projection_two_year_cost' => $this->roundOrNull($currentProjection['cost'], 8),
'projection_two_year_profit' => $this->roundOrNull($currentProjection['profit'], 8),
]);
$offerSummary = array_map(
fn (array $offer): array => $this->enrichOfferScenario(
$offer,
$latestSummary,
$currentHashrateMh,
if ($includeLongTermProjection) {
$currentProjection = $this->projectPerformance(
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
$purchasedMiners
),
$offerSummary
);
$purchasedMiners,
$latestSummary,
730
);
$latestSummary = array_merge($latestSummary, [
'projection_days' => $currentProjection['days'],
'projection_two_year_revenue' => $this->roundOrNull($currentProjection['revenue'], 8),
'projection_two_year_cost' => $this->roundOrNull($currentProjection['cost'], 8),
'projection_two_year_profit' => $this->roundOrNull($currentProjection['profit'], 8),
]);
}
if ($includeOfferScenarios) {
$offerSummary = array_map(
fn (array $offer): array => $this->enrichOfferScenario(
$offer,
$latestSummary,
$currentHashrateMh,
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
$purchasedMiners
),
$offerSummary
);
} else {
$offerSummary = [];
}
return [
'latest_measurement' => $latestSummary,