From 6cbf76918c74c12c8cedd9c2676a72d291823f79 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Sat, 2 May 2026 02:03:42 +0200 Subject: [PATCH] sdfsf --- modules/mining-checker/assets/js/app.js | 9 +- modules/mining-checker/src/Api/Router.php | 105 ++++++++++++++-- .../src/Domain/AnalyticsService.php | 114 ++++++++++++------ .../src/Infrastructure/MiningRepository.php | 17 +++ 4 files changed, 195 insertions(+), 50 deletions(-) diff --git a/modules/mining-checker/assets/js/app.js b/modules/mining-checker/assets/js/app.js index 7d6dd6d..43e6d42 100644 --- a/modules/mining-checker/assets/js/app.js +++ b/modules/mining-checker/assets/js/app.js @@ -1045,7 +1045,8 @@ return; } - const data = await request(`${apiBase}/projects/${encodeURIComponent(key)}/bootstrap`, { timeoutMs: 10000 }); + const params = new URLSearchParams({ view: activeTab || 'overview' }); + const data = await request(`${apiBase}/projects/${encodeURIComponent(key)}/bootstrap?${params.toString()}`, { timeoutMs: 10000 }); const normalized = normalizeBootstrap(data, key); setPayload(normalized); setSettingsForm({ @@ -2109,9 +2110,9 @@ }), ]), h('div', { key: 'charts', className: 'mc-overview-grid' }, [ - panel('Mining-Verlauf', 'Coins total ueber die Zeit.', h(SimpleChart, { type: 'line', data: overviewCharts.mining })), - panel('Performance-Verlauf', 'Letzte DOGE-pro-Tag-Raten je Intervall.', h(SimpleChart, { type: 'area', data: overviewCharts.performance })), - panel('Kurs-Verlauf', 'Historische Preiswerte der Messreihe.', h(SimpleChart, { type: 'line', data: overviewCharts.pricing })), + 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('Zielmonitor', 'Rest-DOGE und Resttage werden gegen den letzten verfuegbaren Kurs je Zielwaehrung berechnet.', h('div', { className: 'mc-target-grid' }, diff --git a/modules/mining-checker/src/Api/Router.php b/modules/mining-checker/src/Api/Router.php index bbc9f95..494e7bb 100644 --- a/modules/mining-checker/src/Api/Router.php +++ b/modules/mining-checker/src/Api/Router.php @@ -22,6 +22,7 @@ final class Router private const BOOTSTRAP_MEASUREMENT_LIMIT = 150; private const BOOTSTRAP_SNAPSHOT_LIMIT = 40; private const LONG_REQUEST_BUDGET_SECONDS = 8.0; + private const OVERVIEW_WINDOW_DAYS = 15; private string $moduleBasePath; private ModuleConfig $config; @@ -164,7 +165,8 @@ final class Router } if ($resource === 'bootstrap' && $method === 'GET') { - $this->respond(['data' => $this->bootstrap($projectKey)]); + $view = trim((string) ($_GET['view'] ?? 'overview')); + $this->respond(['data' => $this->bootstrap($projectKey, $view)]); } if ($resource === 'measurements' && $method === 'GET') { @@ -310,11 +312,13 @@ final class Router } } - private function bootstrap(string $projectKey): array + private function bootstrap(string $projectKey, string $view = 'overview'): array { $startedAt = microtime(true); + $view = $this->normalizeBootstrapView($view); $this->debug->add('bootstrap.start', [ 'project_key' => $projectKey, + 'view' => $view, 'measurement_limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT, 'snapshot_limit' => self::BOOTSTRAP_SNAPSHOT_LIMIT, ]); @@ -339,25 +343,30 @@ final class Router 'purchased_miners' => [], 'measurement_rates' => [], ], ['project_key' => $projectKey]); - $measurements = $this->safeTimed('bootstrap.measurements', fn () => $this->bootstrapMeasurements($projectKey, $settings), [], [ + $measurements = $this->safeTimed('bootstrap.measurements', fn () => $this->bootstrapMeasurements($projectKey, $settings, $view), [], [ 'project_key' => $projectKey, + 'view' => $view, ]); - $targets = $this->safeTimed('bootstrap.targets', fn () => $this->targets($projectKey), [], [ + $targets = $this->safeTimed('bootstrap.targets', fn () => $this->bootstrapTargets($projectKey, $view), [], [ 'project_key' => $projectKey, + 'view' => $view, ]); - $dashboards = $this->safeTimed('bootstrap.dashboards', fn () => $this->dashboards($projectKey), [], [ + $dashboards = $this->safeTimed('bootstrap.dashboards', fn () => $this->bootstrapDashboards($projectKey, $view), [], [ 'project_key' => $projectKey, + 'view' => $view, ]); - $fxSnapshots = $this->safeTimed('bootstrap.fx_snapshots', fn () => $this->measurementFxSnapshots($measurements, self::BOOTSTRAP_SNAPSHOT_LIMIT), [], [ + $fxSnapshots = $this->safeTimed('bootstrap.fx_snapshots', fn () => $this->bootstrapFxSnapshots($measurements, $view), [], [ + 'view' => $view, 'measurement_count' => is_array($measurements) ? count($measurements) : 0, ]); - $summary = $this->safeTimed('bootstrap.summary', fn () => $this->analytics()->buildSummary($measurements, $settings, $targets), [ + $summary = $this->safeTimed('bootstrap.summary', fn () => $this->bootstrapSummary($measurements, $settings, $targets, $view), [ 'latest_measurement' => $measurements !== [] ? $measurements[array_key_last($measurements)] : null, 'baseline' => $settings, 'targets' => [], 'payouts' => [], 'miner_offers' => [], ], [ + 'view' => $view, 'measurement_count' => is_array($measurements) ? count($measurements) : 0, 'target_count' => is_array($targets) ? count($targets) : 0, ]); @@ -381,6 +390,8 @@ final class Router 'summary' => $summary, 'bootstrap_meta' => [ 'degraded' => $measurementCount >= self::BOOTSTRAP_MEASUREMENT_LIMIT, + 'view' => $view, + 'overview_window_days' => self::OVERVIEW_WINDOW_DAYS, 'measurement_limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT, 'snapshot_limit' => self::BOOTSTRAP_SNAPSHOT_LIMIT, 'measurement_count' => $measurementCount, @@ -1254,17 +1265,93 @@ final class Router return $this->analytics()->enrichMeasurements($rows, $settings); } - private function bootstrapMeasurements(string $projectKey, array $settings): array + private function bootstrapMeasurements(string $projectKey, array $settings, string $view): array { - $rows = $this->repository()->listMeasurements($projectKey, self::BOOTSTRAP_MEASUREMENT_LIMIT); + if (in_array($view, ['settings', 'currencies', 'dashboards'], true)) { + return []; + } + + $rows = in_array($view, ['overview', 'mining'], true) + ? $this->repository()->listRecentMeasurements($projectKey, self::BOOTSTRAP_MEASUREMENT_LIMIT) + : $this->repository()->listMeasurements($projectKey, self::BOOTSTRAP_MEASUREMENT_LIMIT); + + if (in_array($view, ['overview', 'mining'], true)) { + $rows = $this->filterMeasurementsToRecentWindow($rows, self::OVERVIEW_WINDOW_DAYS); + } + $this->debug->add('bootstrap.measurements.loaded', [ 'project_key' => $projectKey, + 'view' => $view, 'row_count' => count($rows), 'limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT, ]); return $this->analytics()->enrichMeasurements($rows, $settings); } + private function bootstrapTargets(string $projectKey, string $view): array + { + return in_array($view, ['overview', 'mining'], true) ? $this->targets($projectKey) : []; + } + + private function bootstrapDashboards(string $projectKey, string $view): array + { + return $view === 'dashboards' ? $this->dashboards($projectKey) : []; + } + + private function bootstrapFxSnapshots(array $measurements, string $view): array + { + if (!in_array($view, ['overview', 'mining', 'measurements'], true)) { + return []; + } + + return $this->measurementFxSnapshots($measurements, self::BOOTSTRAP_SNAPSHOT_LIMIT); + } + + private function bootstrapSummary(array $measurements, array $settings, array $targets, string $view): array + { + if (!in_array($view, ['overview', 'mining'], true)) { + return [ + 'latest_measurement' => $measurements !== [] ? $measurements[array_key_last($measurements)] : null, + 'baseline' => $settings, + 'targets' => [], + 'payouts' => [], + 'miner_offers' => [], + ]; + } + + return $this->analytics()->buildSummary($measurements, $settings, $targets); + } + + private function filterMeasurementsToRecentWindow(array $rows, int $windowDays): array + { + if ($rows === [] || $windowDays <= 0) { + return $rows; + } + + $latest = $rows[array_key_last($rows)] ?? null; + $latestTs = is_array($latest) ? strtotime((string) ($latest['measured_at'] ?? '')) : false; + if ($latestTs === false) { + return $rows; + } + + $minTs = $latestTs - ($windowDays * 86400); + $filtered = array_values(array_filter($rows, static function (array $row) use ($minTs): bool { + $measuredTs = strtotime((string) ($row['measured_at'] ?? '')); + return $measuredTs !== false && $measuredTs >= $minTs; + })); + + $latestRow = $rows[array_key_last($rows)] ?? null; + return $filtered !== [] || !is_array($latestRow) ? $filtered : [$latestRow]; + } + + private function normalizeBootstrapView(string $view): string + { + $normalized = trim(strtolower($view)); + return in_array($normalized, ['overview', 'measurements', 'dashboards', 'currencies', 'mining', 'settings'], true) + ? $normalized + : 'overview'; + } + private function createMeasurement(string $projectKey, array $input): array { $projectTimezone = $this->projectTimezone($projectKey); diff --git a/modules/mining-checker/src/Domain/AnalyticsService.php b/modules/mining-checker/src/Domain/AnalyticsService.php index 5c16448..f01caff 100644 --- a/modules/mining-checker/src/Domain/AnalyticsService.php +++ b/modules/mining-checker/src/Domain/AnalyticsService.php @@ -868,50 +868,58 @@ final class AnalyticsService $dogePerDayPerMh = $dogePerDay / $currentHashrateMh; $revenue = 0.0; $cost = 0.0; + $hashrateDayTotal = 0.0; - for ($day = 0; $day < $days; $day++) { - $checkTs = $baseTs + ($day * 86400); - $activeHashrate = 0.0; - - foreach ($costPlans as $plan) { - if (empty($plan['is_active']) || !$this->entryIsCovered($plan, $checkTs)) { - continue; - } - - $activeHashrate += $this->normalizeHashrateMh($plan['mining_speed_value'] ?? null, $plan['mining_speed_unit'] ?? null); - $activeHashrate += $this->normalizeHashrateMh($plan['bonus_speed_value'] ?? null, $plan['bonus_speed_unit'] ?? null); - - $runtimeMonths = (int) ($plan['runtime_months'] ?? 0); - if ($runtimeMonths > 0 && is_numeric($plan['total_cost_amount'] ?? null)) { - $runtimeDays = $runtimeMonths * 30.4375; - $dailyCost = $this->convertAmount((float) $plan['total_cost_amount'] / $runtimeDays, (string) ($plan['currency'] ?? ''), $currency, $latest); - if ($dailyCost !== null) { - $cost += $dailyCost; - } - } + foreach ($costPlans as $plan) { + if (empty($plan['is_active'])) { + continue; } - foreach ($purchasedMiners as $miner) { - if ((array_key_exists('is_active', $miner) && empty($miner['is_active'])) || !$this->entryIsCovered($miner, $checkTs, 'purchased_at')) { - continue; - } - - $activeHashrate += $this->normalizeHashrateMh($miner['mining_speed_value'] ?? null, $miner['mining_speed_unit'] ?? null); - $activeHashrate += $this->normalizeHashrateMh($miner['bonus_speed_value'] ?? null, $miner['bonus_speed_unit'] ?? null); - - $runtimeMonths = (int) ($miner['runtime_months'] ?? 0); - if ($runtimeMonths > 0 && is_numeric($miner['total_cost_amount'] ?? null)) { - $runtimeDays = $runtimeMonths * 30.4375; - $dailyCost = $this->convertAmount((float) $miner['total_cost_amount'] / $runtimeDays, (string) ($miner['currency'] ?? ''), $currency, $latest); - if ($dailyCost !== null) { - $cost += $dailyCost; - } - } + $coveredDays = $this->entryCoveredDayCount($plan, $baseTs, $days); + if ($coveredDays <= 0) { + continue; } - $revenue += $activeHashrate * $dogePerDayPerMh * $pricePerCoin; + $entryHashrateMh = $this->normalizeHashrateMh($plan['mining_speed_value'] ?? null, $plan['mining_speed_unit'] ?? null) + + $this->normalizeHashrateMh($plan['bonus_speed_value'] ?? null, $plan['bonus_speed_unit'] ?? null); + $hashrateDayTotal += $entryHashrateMh * $coveredDays; + + $runtimeMonths = (int) ($plan['runtime_months'] ?? 0); + if ($runtimeMonths > 0 && is_numeric($plan['total_cost_amount'] ?? null)) { + $runtimeDays = $runtimeMonths * 30.4375; + $dailyCost = $this->convertAmount((float) $plan['total_cost_amount'] / $runtimeDays, (string) ($plan['currency'] ?? ''), $currency, $latest); + if ($dailyCost !== null) { + $cost += $dailyCost * $coveredDays; + } + } } + foreach ($purchasedMiners as $miner) { + if (array_key_exists('is_active', $miner) && empty($miner['is_active'])) { + continue; + } + + $coveredDays = $this->entryCoveredDayCount($miner, $baseTs, $days, 'purchased_at'); + if ($coveredDays <= 0) { + continue; + } + + $entryHashrateMh = $this->normalizeHashrateMh($miner['mining_speed_value'] ?? null, $miner['mining_speed_unit'] ?? null) + + $this->normalizeHashrateMh($miner['bonus_speed_value'] ?? null, $miner['bonus_speed_unit'] ?? null); + $hashrateDayTotal += $entryHashrateMh * $coveredDays; + + $runtimeMonths = (int) ($miner['runtime_months'] ?? 0); + if ($runtimeMonths > 0 && is_numeric($miner['total_cost_amount'] ?? null)) { + $runtimeDays = $runtimeMonths * 30.4375; + $dailyCost = $this->convertAmount((float) $miner['total_cost_amount'] / $runtimeDays, (string) ($miner['currency'] ?? ''), $currency, $latest); + if ($dailyCost !== null) { + $cost += $dailyCost * $coveredDays; + } + } + } + + $revenue = $hashrateDayTotal * $dogePerDayPerMh * $pricePerCoin; + return [ 'days' => $days, 'revenue' => $revenue, @@ -920,6 +928,38 @@ final class AnalyticsService ]; } + private function entryCoveredDayCount(array $entry, int $baseTs, int $days, string $startField = 'starts_at'): int + { + if ($days <= 0) { + return 0; + } + + $runtimeMonths = (int) ($entry['runtime_months'] ?? 0); + $startTs = $this->utcTimestamp((string) ($entry[$startField] ?? '')); + if ($startTs <= 0) { + return $days; + } + + $startIndex = max(0, (int) ceil(($startTs - $baseTs) / 86400)); + if ($startIndex >= $days) { + return 0; + } + + if (!empty($entry['auto_renew']) || $runtimeMonths <= 0) { + return $days - $startIndex; + } + + $runtimeDays = $runtimeMonths * 30.4375; + $endTs = (int) round($startTs + ($runtimeDays * 86400)); + $endIndex = (int) floor(($endTs - $baseTs) / 86400); + $endIndex = min($days - 1, $endIndex); + if ($endIndex < $startIndex) { + return 0; + } + + return $endIndex - $startIndex + 1; + } + private function utcTimestamp(?string $value): int { $normalized = trim((string) $value); diff --git a/modules/mining-checker/src/Infrastructure/MiningRepository.php b/modules/mining-checker/src/Infrastructure/MiningRepository.php index 2a61a3d..7dfb6fb 100644 --- a/modules/mining-checker/src/Infrastructure/MiningRepository.php +++ b/modules/mining-checker/src/Infrastructure/MiningRepository.php @@ -410,6 +410,23 @@ final class MiningRepository return $this->normalizeRows($stmt->fetchAll() ?: []); } + public function listRecentMeasurements(string $projectKey, int $limit = 200): array + { + $stmt = $this->pdo->prepare( + 'SELECT * FROM ' . $this->table('measurements') . ' + WHERE project_key = :project_key AND owner_sub = :owner_sub + ORDER BY measured_at DESC + LIMIT :limit' + ); + $stmt->bindValue(':project_key', $projectKey, PDO::PARAM_STR); + $stmt->bindValue(':owner_sub', $this->ownerSub, PDO::PARAM_STR); + $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); + $stmt->execute(); + + $rows = $this->normalizeRows($stmt->fetchAll() ?: []); + return array_reverse($rows); + } + public function listAllMeasurements(string $projectKey): array { $stmt = $this->pdo->prepare(