From 7e8b7f336be4a09f0f1e877311f6030d4995a020 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Fri, 14 Aug 2026 01:08:17 +0200 Subject: [PATCH] Server --- custom/apps/mining-checker/assets/js/app.js | 161 ++++++++++++------ custom/apps/mining-checker/src/Api/Router.php | 46 ++++- .../src/Domain/AnalyticsService.php | 53 +++++- 3 files changed, 201 insertions(+), 59 deletions(-) diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index 897ca2b0..161e0ad5 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -1129,26 +1129,71 @@ const selectableFiatCurrencies = preferredSelectableFiatCurrencies.length ? preferredSelectableFiatCurrencies : fiatCurrencies; const selectableCryptoCurrencies = preferredSelectableCryptoCurrencies.length ? preferredSelectableCryptoCurrencies : cryptoCurrencies; const selectedMinerScenario = scenarioMinerOffers.find((offer) => String(offer.id) === String(selectedMinerScenarioId)) || null; - const activeMinerRows = currentPurchasedMiners.map((miner) => { - const coverage = entryCoverageMeta(miner.purchased_at, miner.runtime_months, miner.auto_renew); + const allMinerRows = currentPurchasedMiners.map((miner) => { const effectiveCurrency = String(miner.currency || '').toUpperCase(); + const paymentType = isCryptoCurrencyCode(effectiveCurrency) ? 'crypto' : 'fiat'; + const effectiveAutoRenew = paymentType === 'crypto' ? false : !!miner.auto_renew; + const coverage = entryCoverageMeta(miner.purchased_at, miner.runtime_months, effectiveAutoRenew); + const referenceCurrency = String(miner.reference_price_currency || '').toUpperCase(); + const referenceAmount = Number(miner.reference_price_amount); + const hasFiatReference = referenceCurrency && !isCryptoCurrencyCode(referenceCurrency); + const fallbackUsdReference = Number(miner.usd_reference_amount); + let baseAmount = null; + let baseCurrency = null; + if (paymentType === 'crypto') { + if (Number.isFinite(referenceAmount) && referenceAmount > 0 && hasFiatReference) { + const convertedReference = referenceCurrency === reportCurrency + ? referenceAmount + : convertCurrencyValue(referenceAmount, referenceCurrency, reportCurrency); + if (Number.isFinite(convertedReference) && convertedReference > 0) { + baseAmount = convertedReference; + baseCurrency = reportCurrency; + } else { + baseAmount = referenceAmount; + baseCurrency = referenceCurrency; + } + } else if (Number.isFinite(fallbackUsdReference) && fallbackUsdReference > 0) { + const convertedUsd = reportCurrency === 'USD' + ? fallbackUsdReference + : convertCurrencyValue(fallbackUsdReference, 'USD', reportCurrency); + if (Number.isFinite(convertedUsd) && convertedUsd > 0) { + baseAmount = convertedUsd; + baseCurrency = reportCurrency; + } else { + baseAmount = fallbackUsdReference; + baseCurrency = 'USD'; + } + } else { + const convertedEffective = convertCurrencyValue(miner.total_cost_amount, effectiveCurrency, reportCurrency); + if (Number.isFinite(convertedEffective) && convertedEffective > 0) { + baseAmount = convertedEffective; + baseCurrency = reportCurrency; + } + } + } else if (Number.isFinite(referenceAmount) && referenceAmount > 0 && hasFiatReference) { + baseAmount = referenceAmount; + baseCurrency = referenceCurrency; + } else if (Number.isFinite(fallbackUsdReference) && fallbackUsdReference > 0) { + baseAmount = fallbackUsdReference; + baseCurrency = 'USD'; + } return { id: `purchase-${miner.id}`, source: 'miete', starts_at: miner.purchased_at, label: miner.label, runtime_months: miner.runtime_months, - auto_renew: !!miner.auto_renew, + auto_renew: effectiveAutoRenew, effective_amount: miner.total_cost_amount, effective_currency: miner.currency, - base_amount: miner.reference_price_amount, - base_currency: miner.reference_price_currency, + base_amount: baseAmount, + base_currency: baseCurrency, miner_id: miner.id, miner_offer_id: miner.miner_offer_id, - payment_type: isCryptoCurrencyCode(effectiveCurrency) ? 'crypto' : 'fiat', + payment_type: paymentType, is_active: miner.is_active !== false && coverage.isCovered, end_at: coverage.endAt, - can_toggle_auto_renew: Number(miner.runtime_months) > 0, + can_toggle_auto_renew: paymentType !== 'crypto' && Number(miner.runtime_months) > 0, toggle_resource: 'purchased-miners', toggle_id: miner.id, hashrate_text: formatHashrateWithBonus(miner.mining_speed_value, miner.mining_speed_unit, miner.bonus_speed_value, miner.bonus_speed_unit), @@ -1177,6 +1222,8 @@ type_label: 'Manuell eingetragen', }; })).sort((left, right) => String(right.starts_at || '').localeCompare(String(left.starts_at || ''))); + const activeMinerRows = allMinerRows.filter((row) => row.is_active); + const inactiveMinerRows = allMinerRows.filter((row) => !row.is_active); useEffect(() => { if (reportCurrencyOverride) { @@ -2745,8 +2792,8 @@ const totalEarnedOverall = latest && latest.growth_since_baseline !== null && latest.growth_since_baseline !== undefined ? Number(latest.growth_since_baseline) : null; - const totalEarnedOverallFiat = latest && totalEarnedOverall !== null - ? convertMeasurementMoney(latest, totalEarnedOverall, reportCurrency) + const totalEarnedOverallFiat = totalEarnedOverall !== null + ? convertCurrencyValue(totalEarnedOverall, currentCoinCurrency, reportCurrency) : null; const totalSpentOverallCurrentAsset = latest ? convertMeasurementMoney( @@ -2755,8 +2802,8 @@ currentCoinCurrency ) : null; - const totalSpentOverallFiat = latest && totalSpentOverallCurrentAsset !== null - ? convertMeasurementMoney(latest, totalSpentOverallCurrentAsset, reportCurrency) + const totalSpentOverallFiat = totalSpentOverallCurrentAsset !== null + ? convertCurrencyValue(totalSpentOverallCurrentAsset, currentCoinCurrency, reportCurrency) : null; const spentByType = activeMinerRows.reduce((summary, row) => { const amount = Number(row?.effective_amount); @@ -3206,6 +3253,51 @@ const scenarioInvestedCapital = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_invested_capital, reportCurrency) : null; const scenarioOfferCost = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_offer_cost, reportCurrency) : null; const scenarioBreakEvenRemaining = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_break_even_remaining_amount, reportCurrency) : null; + const renderMinerTable = (rows, emptyText) => ( + h('div', { className: 'mc-table-shell' }, [ + h('table', { key: 'table', className: 'mc-table' }, [ + h('thead', { key: 'head' }, h('tr', null, ['Label', 'Start', 'Laufzeit', 'Auto', 'Kosten', 'Waehrung', 'Aktiv', 'Aktion'].map((label) => h('th', { key: label }, label)))), + h('tbody', { key: 'body' }, + rows.length + ? rows.map((row) => h('tr', { key: row.id }, [ + h('td', { key: 'label' }, [ + h('div', { key: 'main' }, row.label), + h('div', { key: 'type', className: 'mc-kicker' }, row.type_label), + ]), + h('td', { key: 'start' }, fmtDateTime(row.starts_at)), + h('td', { key: 'runtime' }, [ + h('div', { key: 'months' }, `${row.runtime_months} Monate`), + h('div', { key: 'hash', className: 'mc-kicker' }, row.hashrate_text), + row.end_at ? h('div', { key: 'end', className: 'mc-kicker' }, `Ende ${fmtDate(row.end_at)}`) : null, + ]), + h('td', { key: 'renew' }, row.payment_type === 'crypto' ? 'nein' : (row.auto_renew ? 'ja' : 'nein')), + h('td', { key: 'cost' }, [ + h('div', { key: 'effective' }, fmtNumber(row.effective_amount, 6)), + row.base_amount !== null && row.base_amount !== undefined && row.base_currency + ? h('div', { key: 'base', className: 'mc-kicker' }, `Basis ${fmtNumber(row.base_amount, 6)} ${row.base_currency}`) + : null, + ]), + h('td', { key: 'currency' }, [ + h('div', { key: 'currency-main' }, row.effective_currency), + row.payment_type ? h('div', { key: 'currency-mode', className: 'mc-kicker' }, row.payment_type === 'crypto' ? 'Zahlung Krypto' : 'Zahlung FIAT') : null, + ]), + h('td', { key: 'active' }, row.is_active ? 'ja' : 'nein'), + h('td', { key: 'action' }, + row.can_toggle_auto_renew + ? h('button', { + type: 'button', + className: 'mc-button mc-button--ghost', + onClick: () => toggleMinerAutoRenew(row), + disabled: saving, + }, row.auto_renew ? 'Verlaengerung aus' : 'Verlaengerung an') + : '—' + ), + ])) + : [h('tr', { key: 'empty' }, h('td', { colSpan: 8 }, emptyText))] + ), + ]), + ]) + ); return h('div', { className: 'mc-stack' }, [ panel('Aktive Miner', 'Alle bereits gemieteten oder manuell eingetragenen Miner in einer gemeinsamen Liste.', [ h('div', { key: 'actions', className: 'mc-inline-row' }, [ @@ -3240,49 +3332,10 @@ disabled: !scenarioMinerOffers.length, }, 'Neuen Miner mieten'), ]), - h('div', { key: 'list', className: 'mc-table-shell' }, [ - h('table', { key: 'table', className: 'mc-table' }, [ - h('thead', { key: 'head' }, h('tr', null, ['Label', 'Start', 'Laufzeit', 'Auto', 'Kosten', 'Waehrung', 'Aktiv', 'Aktion'].map((label) => h('th', { key: label }, label)))), - h('tbody', { key: 'body' }, - activeMinerRows.length - ? activeMinerRows.map((row) => h('tr', { key: row.id }, [ - h('td', { key: 'label' }, [ - h('div', { key: 'main' }, row.label), - h('div', { key: 'type', className: 'mc-kicker' }, row.type_label), - ]), - h('td', { key: 'start' }, fmtDateTime(row.starts_at)), - h('td', { key: 'runtime' }, [ - h('div', { key: 'months' }, `${row.runtime_months} Monate`), - h('div', { key: 'hash', className: 'mc-kicker' }, row.hashrate_text), - row.end_at ? h('div', { key: 'end', className: 'mc-kicker' }, `Ende ${fmtDate(row.end_at)}`) : null, - ]), - h('td', { key: 'renew' }, row.auto_renew ? 'ja' : 'nein'), - h('td', { key: 'cost' }, [ - h('div', { key: 'effective' }, fmtNumber(row.effective_amount, 6)), - row.base_amount !== null && row.base_amount !== undefined && row.base_currency - ? h('div', { key: 'base', className: 'mc-kicker' }, `Basis ${fmtNumber(row.base_amount, 6)} ${row.base_currency}`) - : null, - ]), - h('td', { key: 'currency' }, [ - h('div', { key: 'currency-main' }, row.effective_currency), - row.payment_type ? h('div', { key: 'currency-mode', className: 'mc-kicker' }, row.payment_type === 'crypto' ? 'Zahlung Krypto' : 'Zahlung FIAT') : null, - ]), - h('td', { key: 'active' }, row.is_active ? 'ja' : 'nein'), - h('td', { key: 'action' }, - row.can_toggle_auto_renew - ? h('button', { - type: 'button', - className: 'mc-button mc-button--ghost', - onClick: () => toggleMinerAutoRenew(row), - disabled: saving, - }, row.auto_renew ? 'Verlaengerung aus' : 'Verlaengerung an') - : '—' - ), - ])) - : [h('tr', { key: 'empty' }, h('td', { colSpan: 8 }, 'Noch keine Miner hinterlegt.'))] - ), - ]), - ]), + renderMinerTable(activeMinerRows, 'Noch keine aktiven Miner hinterlegt.'), + ]), + panel('Inaktive Miner', 'Abgelaufene oder deaktivierte Miner werden separat gefuehrt.', [ + renderMinerTable(inactiveMinerRows, 'Noch keine inaktiven Miner hinterlegt.'), ]), panel('Miner-Angebote', selectedOfferType === 'crypto' ? (currentWalletMiningBalance !== null diff --git a/custom/apps/mining-checker/src/Api/Router.php b/custom/apps/mining-checker/src/Api/Router.php index 6825e7f1..c64ad767 100644 --- a/custom/apps/mining-checker/src/Api/Router.php +++ b/custom/apps/mining-checker/src/Api/Router.php @@ -1649,7 +1649,7 @@ final class Router private function costPlans(string $projectKey): array { - return $this->repository()->listCostPlans($projectKey); + return array_map(fn (array $plan): array => $this->normalizeLegacyHashrateEntry($plan), $this->repository()->listCostPlans($projectKey)); } private function payouts(string $projectKey): array @@ -1770,7 +1770,7 @@ final class Router private function purchasedMiners(string $projectKey): array { - return $this->repository()->listPurchasedMiners($projectKey); + return array_map(fn (array $miner): array => $this->normalizeLegacyHashrateEntry($miner), $this->repository()->listPurchasedMiners($projectKey)); } private function saveTarget(string $projectKey, array $input): array @@ -2392,6 +2392,48 @@ final class Router return $unit; } + private function normalizeLegacyHashrateEntry(array $entry): array + { + $label = trim((string) ($entry['label'] ?? '')); + $speedValue = is_numeric($entry['mining_speed_value'] ?? null) ? (float) $entry['mining_speed_value'] : null; + $speedUnit = trim((string) ($entry['mining_speed_unit'] ?? '')); + $bonusValue = is_numeric($entry['bonus_speed_value'] ?? null) ? (float) $entry['bonus_speed_value'] : null; + $bonusUnit = trim((string) ($entry['bonus_speed_unit'] ?? '')); + $expectedKhValue = $this->legacyExpectedKhValueFromLabel($label); + + if ($expectedKhValue === null) { + return $entry; + } + + if ($speedUnit === 'MH/s' && $speedValue !== null && abs($speedValue - $expectedKhValue) < 0.0001) { + $entry['mining_speed_unit'] = 'kH/s'; + } + + $expectedBonus = round($expectedKhValue * 0.1, 4); + if ($bonusUnit === 'MH/s' && $bonusValue !== null && abs($bonusValue - $expectedBonus) < 0.0001) { + $entry['bonus_speed_unit'] = 'kH/s'; + } + + return $entry; + } + + private function legacyExpectedKhValueFromLabel(string $label): ?float + { + if ($label === '') { + return null; + } + + if (preg_match('/\b(\d+(?:[.,]\d+)?)\s*kH\/s\b/i', $label, $matches) === 1) { + return (float) str_replace(',', '.', $matches[1]); + } + + if (preg_match('/\b(\d{2,4})\s*er\b/i', $label, $matches) === 1) { + return (float) $matches[1]; + } + + return null; + } + private function requiredPositiveInt(mixed $value, string $field): int { if ($value === null || $value === '' || !is_numeric((string) $value)) { diff --git a/custom/apps/mining-checker/src/Domain/AnalyticsService.php b/custom/apps/mining-checker/src/Domain/AnalyticsService.php index 87170e58..83e783c1 100644 --- a/custom/apps/mining-checker/src/Domain/AnalyticsService.php +++ b/custom/apps/mining-checker/src/Domain/AnalyticsService.php @@ -67,10 +67,10 @@ final class AnalyticsService $fullLatestOnly = !empty($options['full_latest_only']); $baselineCoins = (float) ($settings['baseline_coins_total'] ?? 0.0); $baselineAt = (string) ($settings['baseline_measured_at'] ?? ''); - $costPlans = is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : []; + $costPlans = $this->normalizeLegacyHashrateEntries(is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : []); $payouts = is_array($settings['payouts'] ?? null) ? $settings['payouts'] : []; $walletWithdrawals = is_array($settings['wallet_withdrawals'] ?? null) ? $settings['wallet_withdrawals'] : []; - $purchasedMiners = is_array($settings['purchased_miners'] ?? null) ? $settings['purchased_miners'] : []; + $purchasedMiners = $this->normalizeLegacyHashrateEntries(is_array($settings['purchased_miners'] ?? null) ? $settings['purchased_miners'] : []); $preferredPriceCurrencies = array_values(array_unique(array_filter([ strtoupper(trim((string) ($settings['report_currency'] ?? ''))), 'USD', @@ -319,7 +319,7 @@ final class AnalyticsService $payouts = is_array($settings['payouts'] ?? null) ? $settings['payouts'] : []; $walletWithdrawals = is_array($settings['wallet_withdrawals'] ?? null) ? $settings['wallet_withdrawals'] : []; - $purchasedMiners = is_array($settings['purchased_miners'] ?? null) ? $settings['purchased_miners'] : []; + $purchasedMiners = $this->normalizeLegacyHashrateEntries(is_array($settings['purchased_miners'] ?? null) ? $settings['purchased_miners'] : []); $minerOffers = is_array($settings['miner_offers'] ?? null) ? $settings['miner_offers'] : []; $currentHashrateMh = $this->totalHashrateMh(array_merge( is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [], @@ -1013,6 +1013,53 @@ final class AnalyticsService }; } + private function normalizeLegacyHashrateEntries(array $entries): array + { + return array_map(fn (mixed $entry): mixed => is_array($entry) ? $this->normalizeLegacyHashrateEntry($entry) : $entry, $entries); + } + + private function normalizeLegacyHashrateEntry(array $entry): array + { + $label = trim((string) ($entry['label'] ?? '')); + $speedValue = is_numeric($entry['mining_speed_value'] ?? null) ? (float) $entry['mining_speed_value'] : null; + $speedUnit = trim((string) ($entry['mining_speed_unit'] ?? '')); + $bonusValue = is_numeric($entry['bonus_speed_value'] ?? null) ? (float) $entry['bonus_speed_value'] : null; + $bonusUnit = trim((string) ($entry['bonus_speed_unit'] ?? '')); + $expectedKhValue = $this->legacyExpectedKhValueFromLabel($label); + + if ($expectedKhValue === null) { + return $entry; + } + + if ($speedUnit === 'MH/s' && $speedValue !== null && abs($speedValue - $expectedKhValue) < 0.0001) { + $entry['mining_speed_unit'] = 'kH/s'; + } + + $expectedBonus = round($expectedKhValue * 0.1, 4); + if ($bonusUnit === 'MH/s' && $bonusValue !== null && abs($bonusValue - $expectedBonus) < 0.0001) { + $entry['bonus_speed_unit'] = 'kH/s'; + } + + return $entry; + } + + private function legacyExpectedKhValueFromLabel(string $label): ?float + { + if ($label === '') { + return null; + } + + if (preg_match('/\b(\d+(?:[.,]\d+)?)\s*kH\/s\b/i', $label, $matches) === 1) { + return (float) str_replace(',', '.', $matches[1]); + } + + if (preg_match('/\b(\d{2,4})\s*er\b/i', $label, $matches) === 1) { + return (float) $matches[1]; + } + + return null; + } + private function expandOfferVariants(array $offers, array $settings): array { $expanded = [];