diff --git a/modules/fx-rates/src/Infrastructure/FxRatesRepository.php b/modules/fx-rates/src/Infrastructure/FxRatesRepository.php index d36683d..89dff5e 100644 --- a/modules/fx-rates/src/Infrastructure/FxRatesRepository.php +++ b/modules/fx-rates/src/Infrastructure/FxRatesRepository.php @@ -385,6 +385,30 @@ final class FxRatesRepository } } + public function findFetchByBaseAndFetchedAt(string $baseCurrency, string $fetchedAt): ?array + { + $baseCurrency = strtoupper(trim($baseCurrency)); + $fetchedAt = trim($fetchedAt); + if ($baseCurrency === '' || $fetchedAt === '') { + return null; + } + + $stmt = $this->pdo->prepare( + 'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at + FROM ' . $this->table('fetches') . ' + WHERE base_currency = :base_currency + AND fetched_at = :fetched_at + ORDER BY id ASC + LIMIT 1' + ); + $stmt->execute([ + 'base_currency' => $baseCurrency, + 'fetched_at' => $fetchedAt, + ]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + return is_array($row) ? $this->normalizeFetch($row) : null; + } + private function getFetchById(int $fetchId): ?array { $stmt = $this->pdo->prepare( @@ -496,7 +520,7 @@ final class FxRatesRepository { $source = strtolower(trim($source)); return match ($source) { - 'cron', 'manual', 'api' => $source, + 'cron', 'manual', 'api', 'migration' => $source, default => 'manual', }; } diff --git a/modules/mining-checker/src/Api/Router.php b/modules/mining-checker/src/Api/Router.php index 1c8204a..a3a0d6c 100644 --- a/modules/mining-checker/src/Api/Router.php +++ b/modules/mining-checker/src/Api/Router.php @@ -61,6 +61,7 @@ final class Router public function handle(string $relativePath): never { try { + $this->releaseSessionLock(); $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); $path = trim($relativePath, '/'); @@ -293,11 +294,38 @@ final class Router private function bootstrap(string $projectKey): array { - $settings = $this->settings($projectKey); - $measurements = $this->measurements($projectKey, $settings); - $targets = $this->targets($projectKey); - $dashboards = $this->dashboards($projectKey); - $fxSnapshots = $this->measurementFxSnapshots($measurements); + $startedAt = microtime(true); + $settings = $this->safeRead(fn () => $this->settings($projectKey), [ + 'project_key' => $projectKey, + 'baseline_measured_at' => null, + 'baseline_coins_total' => null, + 'daily_cost_amount' => null, + 'daily_cost_currency' => 'EUR', + 'report_currency' => 'EUR', + 'crypto_currency' => 'DOGE', + 'display_timezone' => 'Europe/Berlin', + 'fx_max_age_hours' => 3, + 'module_theme_mode' => 'inherit', + 'module_theme_accent' => 'teal', + 'preferred_currencies' => ['DOGE', 'USD', 'EUR'], + 'cost_plans' => [], + 'currencies' => [], + 'payouts' => [], + 'miner_offers' => [], + 'purchased_miners' => [], + 'measurement_rates' => [], + ]); + $measurements = $this->safeRead(fn () => $this->measurements($projectKey, $settings, false), []); + $targets = $this->safeRead(fn () => $this->targets($projectKey), []); + $dashboards = $this->safeRead(fn () => $this->dashboards($projectKey), []); + $fxSnapshots = $this->safeRead(fn () => $this->measurementFxSnapshots($measurements), []); + $summary = $this->safeRead(fn () => $this->analytics()->buildSummary($measurements, $settings, $targets), [ + 'latest_measurement' => $measurements !== [] ? $measurements[array_key_last($measurements)] : null, + 'baseline' => $settings, + 'targets' => [], + 'payouts' => [], + 'miner_offers' => [], + ]); return [ 'project' => $this->repository()->getProject($projectKey), @@ -306,7 +334,11 @@ final class Router 'targets' => $targets, 'dashboards' => $dashboards, 'fx_snapshots' => $fxSnapshots, - 'summary' => $this->analytics()->buildSummary($measurements, $settings, $targets), + 'summary' => $summary, + 'bootstrap_meta' => [ + 'degraded' => false, + 'duration_ms' => round((microtime(true) - $startedAt) * 1000, 2), + ], ]; } @@ -752,7 +784,7 @@ final class Router $migratedRates = 0; foreach ($legacyFetches as $legacyFetchId => $legacyFetch) { - $existing = $this->findMatchingFxRatesFetch($legacyFetch); + $existing = $this->findExistingFxFetchForLegacy($fxRepository, $legacyFetch); if (is_array($existing) && !empty($existing['id'])) { $fetchIdMap[$legacyFetchId] = (int) $existing['id']; $reusedFetches++; @@ -803,11 +835,6 @@ final class Router ); $resolvedFetchId = $legacyFetchId !== null ? ($fetchIdMap[$legacyFetchId] ?? null) : null; - if (!is_numeric($resolvedFetchId) || (int) $resolvedFetchId <= 0) { - $nearestSnapshot = $this->fx()->nearestSnapshot(null, (string) ($measurement['measured_at'] ?? ''), null, null); - $resolvedFetchId = is_numeric($nearestSnapshot['id'] ?? null) ? (int) $nearestSnapshot['id'] : null; - } - if (!is_numeric($resolvedFetchId) || (int) $resolvedFetchId <= 0) { $unresolvedMeasurements++; continue; @@ -873,7 +900,7 @@ final class Router return $grouped; } - private function findMatchingFxRatesFetch(array $legacyFetch): ?array + private function findExistingFxFetchForLegacy(object $fxRepository, array $legacyFetch): ?array { $baseCurrency = strtoupper(trim((string) ($legacyFetch['base_currency'] ?? ''))); $fetchedAt = $this->normalizeTimestamp((string) ($legacyFetch['fetched_at'] ?? '')); @@ -881,45 +908,12 @@ final class Router return null; } - $snapshot = $this->fx()->nearestSnapshot($baseCurrency, $fetchedAt, null, 1); - if (!is_array($snapshot)) { + if (!method_exists($fxRepository, 'findFetchByBaseAndFetchedAt')) { return null; } - $snapshotFetchedAt = $this->normalizeTimestamp((string) ($snapshot['fetched_at'] ?? '')); - if ($snapshotFetchedAt !== $fetchedAt) { - return null; - } - - return $this->snapshotMatchesLegacyFetch($snapshot, $legacyFetch) ? $snapshot : null; - } - - private function snapshotMatchesLegacyFetch(array $snapshot, array $legacyFetch): bool - { - $snapshotBase = strtoupper(trim((string) ($snapshot['base_currency'] ?? ''))); - $legacyBase = strtoupper(trim((string) ($legacyFetch['base_currency'] ?? ''))); - if ($snapshotBase === '' || $snapshotBase !== $legacyBase) { - return false; - } - - $snapshotRates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : []; - $legacyRates = is_array($legacyFetch['rates'] ?? null) ? $legacyFetch['rates'] : []; - if ($legacyRates === []) { - return false; - } - - foreach ($legacyRates as $currencyCode => $rate) { - $currencyCode = strtoupper(trim((string) $currencyCode)); - if ($currencyCode === '' || !is_numeric($rate) || !array_key_exists($currencyCode, $snapshotRates) || !is_numeric($snapshotRates[$currencyCode])) { - return false; - } - - if (!$this->ratesAreEquivalent((float) $snapshotRates[$currencyCode], (float) $rate)) { - return false; - } - } - - return true; + $existing = $fxRepository->findFetchByBaseAndFetchedAt($baseCurrency, $fetchedAt); + return is_array($existing) ? $existing : null; } private function resolveLegacyMeasurementFetchId(array $measurement, array $measurementRates, array $legacyFetches): ?int @@ -1062,13 +1056,6 @@ final class Router return abs($leftTs - $rightTs); } - private function ratesAreEquivalent(float $left, float $right): bool - { - $diff = abs($left - $right); - $tolerance = max(0.00000001, max(abs($left), abs($right)) * 0.000001); - return $diff <= $tolerance; - } - private function settings(string $projectKey): array { $settings = $this->repository()->getSettings($projectKey); @@ -1136,11 +1123,13 @@ final class Router return $this->settings($projectKey); } - private function measurements(string $projectKey, ?array $settingsOverride = null): array + private function measurements(string $projectKey, ?array $settingsOverride = null, bool $resolveFxReferences = true): array { $settings = $settingsOverride ?? $this->settings($projectKey); $rows = $this->repository()->listMeasurements($projectKey, 500); - $rows = $this->ensureMeasurementFxReferences($projectKey, $rows, $settings); + if ($resolveFxReferences) { + $rows = $this->ensureMeasurementFxReferences($projectKey, $rows, $settings); + } return $this->analytics()->enrichMeasurements($rows, $settings); } @@ -2125,6 +2114,17 @@ final class Router Http::json($payload, $statusCode); } + private function releaseSessionLock(): void + { + if (PHP_SAPI === 'cli') { + return; + } + + if (session_status() === PHP_SESSION_ACTIVE) { + session_write_close(); + } + } + private function debugLatest(): array { $filePath = DebugState::latestFilePath();