diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index d54b1ab3..6c511025 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -204,6 +204,24 @@ return amount / runtimeDays; } + function toKhPerSecond(value, unit) { + const numericValue = Number(value); + const normalizedUnit = String(unit || '').trim(); + if (!Number.isFinite(numericValue) || numericValue <= 0 || !normalizedUnit) { + return 0; + } + + if (normalizedUnit === 'MH/s') { + return numericValue * 1000; + } + + if (normalizedUnit === 'kH/s') { + return numericValue; + } + + return 0; + } + function recentMeasurementWindow(rows, windowDays) { if (!Array.isArray(rows) || rows.length === 0) { return []; @@ -1066,6 +1084,7 @@ speed_min: '', speed_unit: 'auto', price_max: '', + max_doge: '', runtime_months: '', }); const [costPlanForm, setCostPlanForm] = useState({ @@ -1199,6 +1218,7 @@ .slice(0, 6); const hasCryptoOfferBasis = Number(offerPreviewForm.crypto_base_price_amount) > 0; const hasFiatOfferBasis = Number(offerPreviewForm.fiat_base_price_amount) > 0; + const maxDogeFilter = minerOfferFilters.max_doge === '' ? null : Number(minerOfferFilters.max_doge); const filteredMinerOffers = evaluatedMinerOffers.filter((offer) => { const paymentType = String(offer.payment_type || '').toLowerCase(); const speedMin = minerOfferFilters.speed_min === '' ? null : Number(minerOfferFilters.speed_min); @@ -1211,6 +1231,11 @@ offer.base_price_currency || offer.effective_price_currency, reportCurrency ); + const comparableDogePrice = convertCurrencyValue( + offer.effective_price_amount ?? offer.base_price_amount, + offer.effective_price_currency || offer.base_price_currency, + 'DOGE' + ); const runtime = Number(offer.runtime_months); const comparableHashrate = speedUnit === 'kh' ? offerHashrate * 1000 @@ -1220,7 +1245,19 @@ return false; } - if (selectedOfferType === 'crypto' && !isOfferAvailableForWallet(offer, latestWalletSnapshot)) { + if ( + selectedOfferType === 'crypto' && + Number.isFinite(maxDogeFilter) && + (!Number.isFinite(comparableDogePrice) || comparableDogePrice > maxDogeFilter) + ) { + return false; + } + + if ( + selectedOfferType === 'crypto' && + !Number.isFinite(maxDogeFilter) && + !isOfferAvailableForWallet(offer, latestWalletSnapshot) + ) { return false; } @@ -1242,7 +1279,7 @@ return true; }); - const scenarioMinerOffers = filteredMinerOffers.filter((offer) => isOfferAvailableForWallet(offer, latestWalletSnapshot)); + const scenarioMinerOffers = filteredMinerOffers; const speedUnits = ['kH/s', 'MH/s']; const fiatCurrencies = currencies.filter((currency) => !currency.is_crypto); const cryptoCurrencies = currencies.filter((currency) => !!currency.is_crypto); @@ -1311,6 +1348,8 @@ const dailyReportAmount = Number.isFinite(dailyAmount) && dailyCurrency ? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency)) : null; + const totalHashrateKh = toKhPerSecond(miner.mining_speed_value, miner.mining_speed_unit) + toKhPerSecond(miner.bonus_speed_value, miner.bonus_speed_unit); + const costPerKh = totalHashrateKh > 0 ? (Number(miner.total_cost_amount) / totalHashrateKh) : null; return { id: `purchase-${miner.id}`, source: 'miete', @@ -1323,6 +1362,8 @@ daily_cost_amount: dailyAmount, daily_cost_currency: dailyCurrency, daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null, + total_hashrate_kh: totalHashrateKh > 0 ? totalHashrateKh : null, + cost_per_kh_amount: Number.isFinite(costPerKh) && costPerKh > 0 ? costPerKh : null, base_amount: baseAmount, base_currency: baseCurrency, miner_id: miner.id, @@ -1346,6 +1387,8 @@ const dailyReportAmount = Number.isFinite(dailyAmount) && dailyCurrency ? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency)) : null; + const totalHashrateKh = toKhPerSecond(plan.mining_speed_value, plan.mining_speed_unit) + toKhPerSecond(plan.bonus_speed_value, plan.bonus_speed_unit); + const costPerKh = totalHashrateKh > 0 ? (Number(plan.total_cost_amount) / totalHashrateKh) : null; return { id: `plan-${plan.id}`, source: 'manual', @@ -1358,6 +1401,8 @@ daily_cost_amount: dailyAmount, daily_cost_currency: dailyCurrency, daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null, + total_hashrate_kh: totalHashrateKh > 0 ? totalHashrateKh : null, + cost_per_kh_amount: Number.isFinite(costPerKh) && costPerKh > 0 ? costPerKh : null, base_amount: plan.base_price_amount, base_currency: currentSettings.report_currency || 'EUR', payment_type: plan.payment_type, @@ -3485,7 +3530,7 @@ 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('thead', { key: 'head' }, h('tr', null, ['Label', 'Start', 'Laufzeit', 'Auto', 'Kosten Server', 'Kosten/kH/s', 'Waehrung', 'Aktiv', 'Aktion'].map((label) => h('th', { key: label }, label)))), h('tbody', { key: 'body' }, rows.length ? rows.map((row) => h('tr', { key: row.id }, [ @@ -3500,7 +3545,7 @@ 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('td', { key: 'cost-total' }, [ h('div', { key: 'effective' }, fmtNumber(row.effective_amount, 6)), row.daily_cost_amount !== null && row.daily_cost_amount !== undefined && row.daily_cost_currency ? h('div', { key: 'daily', className: 'mc-kicker' }, [ @@ -3514,6 +3559,14 @@ ? h('div', { key: 'base', className: 'mc-kicker' }, `Basis ${fmtNumber(row.base_amount, 6)} ${row.base_currency}`) : null, ]), + h('td', { key: 'cost-kh' }, row.cost_per_kh_amount !== null && row.cost_per_kh_amount !== undefined + ? [ + h('div', { key: 'amount' }, fmtNumber(row.cost_per_kh_amount, 6)), + row.total_hashrate_kh !== null && row.total_hashrate_kh !== undefined + ? h('div', { key: 'hash', className: 'mc-kicker' }, `bei ${fmtNumber(row.total_hashrate_kh, 4)} kH/s`) + : null, + ] + : 'n/a'), 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, @@ -3530,7 +3583,7 @@ : '—' ), ])) - : [h('tr', { key: 'empty' }, h('td', { colSpan: 8 }, emptyText))] + : [h('tr', { key: 'empty' }, h('td', { colSpan: 9 }, emptyText))] ), ]), ]) @@ -3618,6 +3671,9 @@ { value: 'auto', label: 'MH/s' }, { value: 'kh', label: 'kH/s' }, ], (value) => setMinerOfferFilters({ ...minerOfferFilters, speed_unit: value || 'auto' })), + selectedOfferType === 'crypto' + ? inputField('Max. DOGE', 'number', minerOfferFilters.max_doge, (value) => setMinerOfferFilters({ ...minerOfferFilters, max_doge: value }), '0.0001') + : null, inputField(`Max. Basispreis (${reportCurrency})`, 'number', minerOfferFilters.price_max, (value) => setMinerOfferFilters({ ...minerOfferFilters, price_max: value }), '0.0001'), selectField('Laufzeit', minerOfferFilters.runtime_months, [{ value: '', label: 'Alle Laufzeiten' }].concat(Array.from(new Set(evaluatedMinerOffers.map((offer) => String(offer.runtime_months || '')).filter(Boolean))).sort((a, b) => Number(a) - Number(b)).map((value) => ({ value, diff --git a/custom/apps/mining-checker/src/Domain/AnalyticsService.php b/custom/apps/mining-checker/src/Domain/AnalyticsService.php index 28092be7..5df38f5e 100644 --- a/custom/apps/mining-checker/src/Domain/AnalyticsService.php +++ b/custom/apps/mining-checker/src/Domain/AnalyticsService.php @@ -321,10 +321,12 @@ final class AnalyticsService $walletWithdrawals = is_array($settings['wallet_withdrawals'] ?? null) ? $settings['wallet_withdrawals'] : []; $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( + $latestMeasurementTs = $this->utcTimestamp((string) ($latest['measured_at'] ?? '')); + $currentHashrateMh = $this->measurementHashrateMh( is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [], - $purchasedMiners - )); + $purchasedMiners, + $latestMeasurementTs > 0 ? $latestMeasurementTs : null + ); $offerSummary = []; foreach ($this->expandOfferVariants($minerOffers, $settings) as $offer) { $offerSummary[] = $this->evaluateMinerOffer($offer, $latest, $latestPriceByCurrency, $currentHashrateMh, $settings); @@ -1480,11 +1482,11 @@ final class AnalyticsService $currency = strtoupper(trim((string) ($latest['effective_price_currency'] ?? $latest['price_currency'] ?? ''))); $pricePerCoin = is_numeric($latest['effective_price_per_coin'] ?? null) ? (float) $latest['effective_price_per_coin'] : null; $dogePerDay = is_numeric($latest['doge_per_day_interval'] ?? null) ? (float) $latest['doge_per_day_interval'] : null; - $currentHashrateMh = $this->totalHashrateMh(array_merge($costPlans, $purchasedMiners)); $baseTs = $this->utcTimestamp((string) ($latest['measured_at'] ?? '')); if ($baseTs <= 0) { $baseTs = $this->utcTimestamp($this->currentUtcDateTime()); } + $currentHashrateMh = $this->measurementHashrateMh($costPlans, $purchasedMiners, $baseTs > 0 ? $baseTs : null); if ($currency === '' || $pricePerCoin === null || $dogePerDay === null || $currentHashrateMh <= 0 || $days <= 0) { return ['days' => $days, 'revenue' => null, 'cost' => null, 'profit' => null]; @@ -1881,8 +1883,8 @@ final class AnalyticsService $currency = strtoupper(trim((string) ($latest['effective_price_currency'] ?? $latest['price_currency'] ?? ''))); $pricePerCoin = is_numeric($latest['effective_price_per_coin'] ?? null) ? (float) $latest['effective_price_per_coin'] : null; $dogePerDay = is_numeric($latest['doge_per_day_interval'] ?? null) ? (float) $latest['doge_per_day_interval'] : null; - $currentHashrateMh = $this->totalHashrateMh(array_merge($costPlans, $purchasedMiners)); $baseTs = $this->utcTimestamp((string) ($latest['measured_at'] ?? '')); + $currentHashrateMh = $this->measurementHashrateMh($costPlans, $purchasedMiners, $baseTs > 0 ? $baseTs : null); if ($currency === '' || $pricePerCoin === null || $dogePerDay === null || $currentHashrateMh <= 0 || $baseTs <= 0) { return ['days' => null, 'eta' => null]; diff --git a/public/assets/desktop/desktop.css b/public/assets/desktop/desktop.css index fd4917ae..eb7760d3 100644 --- a/public/assets/desktop/desktop.css +++ b/public/assets/desktop/desktop.css @@ -1679,7 +1679,9 @@ body .tray-pill.tray-pill-status--unconfigured { } .window.is-mobile-fullscreen { + position: fixed !important; left: 0 !important; + right: 0 !important; border-radius: 0; border: 0; box-shadow: none; diff --git a/public/assets/desktop/desktop.js b/public/assets/desktop/desktop.js index 5553e734..78f02c3d 100644 --- a/public/assets/desktop/desktop.js +++ b/public/assets/desktop/desktop.js @@ -1185,16 +1185,22 @@ if (payloadNode) { if (isMobileViewport()) { const mobileTopInset = maximizeOverSystemBar ? 0 : topDesktopInset; record.node.classList.add('is-mobile-fullscreen'); + record.node.style.position = 'fixed'; record.node.style.left = '0px'; + record.node.style.right = '0px'; record.node.style.top = `${mobileTopInset}px`; - record.node.style.width = '100%'; + record.node.style.bottom = `${bottomDesktopInset}px`; + record.node.style.width = '100vw'; record.node.style.height = maximizeOverSystemBar - ? `calc(100% - ${bottomDesktopInset}px)` - : `calc(100% - ${mobileTopInset + bottomDesktopInset}px)`; + ? `calc(100vh - ${bottomDesktopInset}px)` + : `calc(100vh - ${mobileTopInset + bottomDesktopInset}px)`; return; } record.node.classList.remove('is-mobile-fullscreen'); + record.node.style.position = 'absolute'; + record.node.style.right = ''; + record.node.style.bottom = ''; if (record.state.maximized) { const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;