diff --git a/modules/mining-checker/assets/js/app.js b/modules/mining-checker/assets/js/app.js index 78be5698..5d65e1e1 100644 --- a/modules/mining-checker/assets/js/app.js +++ b/modules/mining-checker/assets/js/app.js @@ -1955,9 +1955,9 @@ applySavedPayout(savedPayout); setWalletTransferRows((previous) => previous.filter((row) => String(row.id) !== String(savedPayout.id)).concat(savedPayout).sort((left, right) => String(left?.payout_at || '').localeCompare(String(right?.payout_at || '')))); invalidateProjectBootstrapCache(projectKey); - setMessage('Wallet-Transfer gespeichert.'); setPayoutForm({ payout_at: '', coins_amount: '', payout_currency: currentSettings.crypto_currency || 'DOGE', note: '' }); setPayoutModalOpen(false); + await reloadBootstrapAfterMutation('Wallet-Transfer gespeichert.'); } catch (err) { setError(err.message); } finally { @@ -1987,7 +1987,7 @@ applySavedPayout(savedPayout); setWalletTransferRows((previous) => previous.filter((row) => String(row.id) !== String(savedPayout.id)).concat(savedPayout).sort((left, right) => String(left?.payout_at || '').localeCompare(String(right?.payout_at || '')))); invalidateProjectBootstrapCache(projectKey); - setMessage('Gesamter aktueller Miner-Bestand wurde ins NC Wallet uebertragen.'); + await reloadBootstrapAfterMutation('Gesamter aktueller Miner-Bestand wurde ins NC Wallet uebertragen.'); } catch (err) { setError(err.message); } finally { diff --git a/modules/mining-checker/src/Domain/AnalyticsService.php b/modules/mining-checker/src/Domain/AnalyticsService.php index 3878f27d..c103821f 100644 --- a/modules/mining-checker/src/Domain/AnalyticsService.php +++ b/modules/mining-checker/src/Domain/AnalyticsService.php @@ -372,24 +372,36 @@ final class AnalyticsService 'reinvest' ) : null; - $walletBalances = $this->walletBalances($payouts, $walletWithdrawals, $purchasedMiners, $latestMeasuredTs); + $walletBalances = $this->walletBalances($payouts, $walletWithdrawals, $purchasedMiners, 0); $latestWalletSnapshot = $this->latestWalletSnapshot(is_array($options['wallet_snapshots'] ?? null) ? $options['wallet_snapshots'] : []); - if (is_array($latestWalletSnapshot)) { + $latestWalletSnapshotTs = is_array($latestWalletSnapshot) + ? $this->utcTimestamp((string) ($latestWalletSnapshot['measured_at'] ?? '')) + : 0; + $latestLedgerActivityTs = $this->latestWalletLedgerActivityTimestamp($payouts, $walletWithdrawals, $purchasedMiners); + $useWalletSnapshot = is_array($latestWalletSnapshot) + && $latestWalletSnapshotTs > 0 + && $latestWalletSnapshotTs >= $latestLedgerActivityTs; + if ($useWalletSnapshot) { $snapshotBalances = $this->walletSnapshotBalances($latestWalletSnapshot); if ($snapshotBalances !== []) { $walletBalances = $snapshotBalances; } } - $walletBalanceCurrentAsset = (float) ($walletBalances[strtoupper(trim((string) ($latest['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE')))] ?? 0.0); - $holdingsCurrentAsset = $walletBalanceCurrentAsset + (float) ($latest['coins_total_visible'] ?? $latest['coins_total'] ?? 0); + $latestAsset = strtoupper(trim((string) ($latest['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE'))); + $postMeasurementPayoutAmount = $this->postMeasurementPayoutAmount($payouts, $latestMeasuredTs, $latestAsset); + $visibleCoinsAtMeasurement = (float) ($latest['coins_total_visible'] ?? $latest['coins_total'] ?? 0.0); + $adjustedVisibleCoins = max(0.0, $visibleCoinsAtMeasurement - $postMeasurementPayoutAmount); + $walletBalanceCurrentAsset = (float) ($walletBalances[$latestAsset] ?? 0.0); + $holdingsCurrentAsset = $walletBalanceCurrentAsset + $adjustedVisibleCoins; $walletValue = $latestCurrency !== '' ? ( - is_array($latestWalletSnapshot) + $useWalletSnapshot ? $this->walletSnapshotValue($latestWalletSnapshot, $latestCurrency, $latest) : $this->walletBalanceValue($walletBalances, $latestCurrency, $latest) ) : null; - $currentVisibleValue = is_numeric($latest['current_value'] ?? null) ? (float) $latest['current_value'] : null; + $effectivePricePerCoin = is_numeric($latest['effective_price_per_coin'] ?? null) ? (float) $latest['effective_price_per_coin'] : null; + $currentVisibleValue = $effectivePricePerCoin !== null ? $adjustedVisibleCoins * $effectivePricePerCoin : null; $totalHoldingsValue = ($walletValue !== null || $currentVisibleValue !== null) ? (float) ($walletValue ?? 0.0) + (float) ($currentVisibleValue ?? 0.0) : null; @@ -407,18 +419,41 @@ final class AnalyticsService $breakEvenRemainingAmount ) : ['days' => null, 'eta' => null]; $breakEvenDaysOverall = is_numeric($breakEvenProjection['days'] ?? null) ? (float) $breakEvenProjection['days'] : null; + $postMeasurementPayout = $this->latestPostMeasurementPayout($payouts, $latestMeasuredTs, $latestAsset); + $lastPayoutAt = $postMeasurementPayout['payout_at'] ?? ($latest['last_payout_at'] ?? null); + $hoursSinceLastPayout = null; + $coinsSinceLastPayout = $latest['coins_since_last_payout'] ?? null; + $perHourSinceLastPayout = $latest['doge_per_hour_since_last_payout'] ?? null; + $perDaySinceLastPayout = $latest['doge_per_day_since_last_payout'] ?? null; + if (is_array($postMeasurementPayout)) { + $postPayoutTs = (int) ($postMeasurementPayout['payout_ts'] ?? 0); + if ($postPayoutTs > 0) { + $hoursSinceLastPayout = max(0.0, (time() - $postPayoutTs) / 3600); + $coinsSinceLastPayout = 0.0; + $perHourSinceLastPayout = 0.0; + $perDaySinceLastPayout = 0.0; + } + } + $latestSummary = array_merge($latest, [ + 'coins_total_visible' => $this->roundOrNull($adjustedVisibleCoins, 6), 'invested_capital' => $this->roundOrNull($cashInvestedCapital, 8), 'cash_invested_capital' => $this->roundOrNull($cashInvestedCapital, 8), 'reinvested_capital' => $this->roundOrNull($reinvestedCapital, 8), 'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAsset, 6), 'holdings_current_asset' => $this->roundOrNull($holdingsCurrentAsset, 6), 'wallet_value' => $this->roundOrNull($walletValue, 8), - 'wallet_snapshot_measured_at' => is_array($latestWalletSnapshot) ? (string) ($latestWalletSnapshot['measured_at'] ?? '') : null, + 'wallet_snapshot_measured_at' => $useWalletSnapshot ? (string) ($latestWalletSnapshot['measured_at'] ?? '') : null, 'total_holdings_value' => $this->roundOrNull($totalHoldingsValue, 8), 'break_even_remaining_amount' => $this->roundOrNull($breakEvenRemainingAmount, 8), 'break_even_days_overall' => $this->roundOrNull($breakEvenDaysOverall, 4), 'break_even_eta_at' => $breakEvenProjection['eta'] ?? null, + 'current_value' => $this->roundOrNull($currentVisibleValue, 8), + 'last_payout_at' => $lastPayoutAt, + 'hours_since_last_payout' => $this->roundOrNull(is_numeric($hoursSinceLastPayout) ? (float) $hoursSinceLastPayout : null, 4), + 'coins_since_last_payout' => $this->roundOrNull(is_numeric($coinsSinceLastPayout) ? (float) $coinsSinceLastPayout : null, 6), + 'doge_per_hour_since_last_payout' => $this->roundOrNull(is_numeric($perHourSinceLastPayout) ? (float) $perHourSinceLastPayout : null, 6), + 'doge_per_day_since_last_payout' => $this->roundOrNull(is_numeric($perDaySinceLastPayout) ? (float) $perDaySinceLastPayout : null, 6), ]); if ($includeLongTermProjection) { $currentProjection = $this->projectPerformance( @@ -457,7 +492,7 @@ final class AnalyticsService 'payouts' => [ 'total_count' => count($payouts), 'total_coins' => $this->roundOrNull(array_sum(array_map(static fn (array $payout): float => (float) ($payout['coins_amount'] ?? 0), $payouts)), 6), - 'current_visible_coins' => $this->roundOrNull((float) ($latest['coins_total_visible'] ?? $latest['coins_total']), 6), + 'current_visible_coins' => $this->roundOrNull($adjustedVisibleCoins, 6), 'current_effective_coins' => $this->roundOrNull((float) ($latest['coins_total_effective'] ?? $latest['coins_total']), 6), 'wallet_balances' => $walletBalances, 'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAsset, 6), @@ -1368,6 +1403,78 @@ final class AnalyticsService return null; } + private function latestWalletLedgerActivityTimestamp(array $payouts, array $walletWithdrawals, array $purchasedMiners): int + { + $latestTs = 0; + + foreach ($payouts as $payout) { + $latestTs = max($latestTs, $this->utcTimestamp((string) ($payout['payout_at'] ?? ''))); + } + + foreach ($walletWithdrawals as $withdrawal) { + $latestTs = max($latestTs, $this->utcTimestamp((string) ($withdrawal['withdrawal_at'] ?? ''))); + } + + foreach ($purchasedMiners as $miner) { + $latestTs = max($latestTs, $this->utcTimestamp((string) ($miner['purchased_at'] ?? ''))); + } + + return $latestTs; + } + + private function postMeasurementPayoutAmount(array $payouts, int $measurementTs, string $asset): float + { + if ($measurementTs <= 0 || $asset === '') { + return 0.0; + } + + $total = 0.0; + foreach ($payouts as $payout) { + $payoutTs = $this->utcTimestamp((string) ($payout['payout_at'] ?? '')); + if ($payoutTs < $measurementTs) { + continue; + } + + $currency = strtoupper(trim((string) ($payout['payout_currency'] ?? ''))); + if ($currency !== $asset) { + continue; + } + + $amount = is_numeric($payout['coins_amount'] ?? null) ? (float) $payout['coins_amount'] : 0.0; + $total += $amount; + } + + return $total; + } + + private function latestPostMeasurementPayout(array $payouts, int $measurementTs, string $asset): ?array + { + if ($measurementTs <= 0 || $asset === '') { + return null; + } + + $latest = null; + foreach ($payouts as $payout) { + $payoutTs = $this->utcTimestamp((string) ($payout['payout_at'] ?? '')); + if ($payoutTs < $measurementTs) { + continue; + } + + $currency = strtoupper(trim((string) ($payout['payout_currency'] ?? ''))); + if ($currency !== $asset) { + continue; + } + + $latest = [ + 'payout_at' => (string) ($payout['payout_at'] ?? ''), + 'payout_ts' => $payoutTs, + 'coins_amount' => is_numeric($payout['coins_amount'] ?? null) ? (float) $payout['coins_amount'] : 0.0, + ]; + } + + return $latest; + } + private function walletSnapshotBalances(array $snapshot): array { $balances = [];