diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index b84d94b1..d697652a 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -1254,7 +1254,6 @@ ).toUpperCase(); return ledgerCurrency || currentMiningCurrency || currentSettings.crypto_currency || 'DOGE'; })(); - const currentWalletMiningBalance = walletPurchasingPower(latestWalletSnapshot, currentMiningCurrency); const preferredCurrencyCodes = Array.isArray(currentSettings.preferred_currencies) ? currentSettings.preferred_currencies.map((code) => String(code || '').toUpperCase()).filter(Boolean) : []; @@ -1305,14 +1304,6 @@ return false; } - if ( - selectedOfferType === 'crypto' && - !Number.isFinite(maxDogeFilter) && - !isOfferAvailableForWallet(offer, latestWalletSnapshot) - ) { - return false; - } - if (selectedOfferType === 'fiat' && !Number.isFinite(speedMin)) { return false; } @@ -1331,7 +1322,18 @@ return true; }); - const scenarioMinerOffers = filteredMinerOffers; + const pinnedReferenceMinerOffer = evaluatedMinerOffers.find((offer) => ( + String(offer.payment_type || '').toLowerCase() === 'crypto' + && Number(offer.runtime_months) === 36 + && Math.abs(toKhPerSecond(offer.mining_speed_value, offer.mining_speed_unit) - 50) < 0.0001 + )) || null; + const visibleMinerOffers = pinnedReferenceMinerOffer && !filteredMinerOffers.some((offer) => String(offer.id) === String(pinnedReferenceMinerOffer.id)) + ? [{ ...pinnedReferenceMinerOffer, is_pinned_reference: true }].concat(filteredMinerOffers) + : filteredMinerOffers.map((offer) => ({ + ...offer, + is_pinned_reference: pinnedReferenceMinerOffer !== null && String(offer.id) === String(pinnedReferenceMinerOffer.id), + })); + const scenarioMinerOffers = visibleMinerOffers; const preferredMinerOffer = currentSummaryMinerOffers .filter((offer) => String(offer.payment_type || '').toLowerCase() === 'crypto') .map((offer) => { @@ -1367,13 +1369,20 @@ offer_hashrate_mh: targetOfferHashrateKh / 1000, }; })(); - const resolvedPreferredMinerOffer = preferredMinerOffer || syntheticPreferredMinerOffer; + const resolvedPreferredMinerOffer = pinnedReferenceMinerOffer || preferredMinerOffer || syntheticPreferredMinerOffer; const preferredOfferRequiredCoins = resolvedPreferredMinerOffer - ? convertCurrencyValue( - resolvedPreferredMinerOffer.effective_price_amount ?? resolvedPreferredMinerOffer.crypto_display_price_amount ?? resolvedPreferredMinerOffer.usd_display_price_amount, - resolvedPreferredMinerOffer.effective_price_currency || resolvedPreferredMinerOffer.crypto_display_price_currency || resolvedPreferredMinerOffer.usd_display_price_currency || currentMiningCurrency, - currentMiningCurrency - ) + ? (() => { + const directCryptoAmount = Number(resolvedPreferredMinerOffer.crypto_display_price_amount); + const directCryptoCurrency = String(resolvedPreferredMinerOffer.crypto_display_price_currency || '').toUpperCase(); + if (Number.isFinite(directCryptoAmount) && directCryptoAmount > 0 && directCryptoCurrency === currentMiningCurrency) { + return directCryptoAmount; + } + return convertCurrencyValue( + resolvedPreferredMinerOffer.effective_price_amount ?? resolvedPreferredMinerOffer.crypto_display_price_amount ?? resolvedPreferredMinerOffer.usd_display_price_amount, + resolvedPreferredMinerOffer.effective_price_currency || resolvedPreferredMinerOffer.crypto_display_price_currency || resolvedPreferredMinerOffer.usd_display_price_currency || currentMiningCurrency, + currentMiningCurrency + ); + })() : null; const preferredOfferCurrentCoins = payload?.summary?.payouts?.holdings_current_asset !== null && payload?.summary?.payouts?.holdings_current_asset !== undefined @@ -1765,91 +1774,6 @@ return rate === null ? null : numericValue * rate; } - function walletAssetBalance(snapshot, currency) { - const code = String(currency || '').toUpperCase(); - if (!snapshot || !code) { - return null; - } - - const assets = snapshot.balances_json && typeof snapshot.balances_json === 'object' ? snapshot.balances_json : {}; - const asset = assets[code] ?? assets[code.toLowerCase()] ?? assets[code.toUpperCase()]; - const snapshotCurrency = String(snapshot.wallet_currency || '').toUpperCase(); - const rawBalance = asset && typeof asset === 'object' - ? asset.balance - : (asset ?? (snapshotCurrency === code ? snapshot.wallet_balance : null)); - const balance = Number(rawBalance); - return Number.isFinite(balance) ? balance : null; - } - - function walletPurchasingPower(snapshot, targetCurrency) { - const target = String(targetCurrency || '').toUpperCase(); - if (!snapshot || !target) { - return null; - } - - const directBalance = walletAssetBalance(snapshot, target); - if (Number.isFinite(directBalance)) { - return directBalance; - } - - const totalValueAmount = Number(snapshot.total_value_amount); - const totalValueCurrency = String(snapshot.total_value_currency || '').toUpperCase(); - if (Number.isFinite(totalValueAmount) && totalValueCurrency) { - const convertedTotal = convertCurrencyValue(totalValueAmount, totalValueCurrency, target); - if (Number.isFinite(convertedTotal)) { - return convertedTotal; - } - } - - const assets = snapshot.balances_json && typeof snapshot.balances_json === 'object' ? snapshot.balances_json : {}; - let total = 0; - let matched = false; - for (const [assetCode, assetValue] of Object.entries(assets)) { - const normalizedCode = String(assetCode || '').toUpperCase(); - const balance = Number(assetValue && typeof assetValue === 'object' ? assetValue.balance : assetValue); - const priceAmount = Number(assetValue && typeof assetValue === 'object' ? assetValue.price_amount : null); - const priceCurrency = String(assetValue && typeof assetValue === 'object' ? assetValue.price_currency : '').toUpperCase(); - if (!normalizedCode || !Number.isFinite(balance) || balance <= 0) { - continue; - } - if (normalizedCode === target) { - total += balance; - matched = true; - continue; - } - if (Number.isFinite(priceAmount) && priceAmount > 0 && priceCurrency) { - const assetValueInPriceCurrency = balance * priceAmount; - const converted = convertCurrencyValue(assetValueInPriceCurrency, priceCurrency, target); - if (Number.isFinite(converted)) { - total += converted; - matched = true; - } - } - } - - return matched ? total : null; - } - - function isOfferAvailableForWallet(offer, walletSnapshot) { - if (!offer || !offer.is_active) { - return false; - } - - if (String(offer.payment_type || '').toLowerCase() !== 'crypto') { - return true; - } - - const currency = String(offer.effective_price_currency || offer.price_currency || '').toUpperCase(); - const price = Number(offer.effective_price_amount); - const balance = Number(walletPurchasingPower(walletSnapshot, currency)); - - if (!currency || !Number.isFinite(price) || !Number.isFinite(balance)) { - return true; - } - - return price <= balance + 0.00000001; - } - function latestFxHistoryRate(fromCurrency, toCurrency) { const from = String(fromCurrency || '').toUpperCase(); const to = String(toCurrency || '').toUpperCase(); @@ -2175,9 +2099,12 @@ } } - const effectivePrice = Number(row.effective_price_per_coin); - const convertedPrice = Number.isFinite(effectivePrice) - ? convertMeasurementMoney(row, effectivePrice, reportCurrency) + const rawPrice = row.effective_price_per_coin !== null && row.effective_price_per_coin !== undefined + ? row.effective_price_per_coin + : row.price_per_coin; + const sourcePrice = Number(rawPrice); + const convertedPrice = Number.isFinite(sourcePrice) && sourcePrice > 0 + ? convertMeasurementMoney(row, sourcePrice, reportCurrency) : null; if (!dayKey || !Number.isFinite(Number(convertedPrice))) { return; @@ -3853,9 +3780,7 @@ renderMinerTable(inactiveMinerRows, 'Noch keine inaktiven Miner hinterlegt.'), ]), panel('Miner-Angebote', selectedOfferType === 'crypto' - ? (currentWalletMiningBalance !== null - ? `Crypto-Angebote sind aktiv. Standardmäßig werden Laufzeiten ab ${fmtNumber(minOfferRuntimeMonths, 0)} Monaten berücksichtigt und nur Angebote gezeigt, die dein NC-Wallet aktuell tragen kann (${fmtNumber(currentWalletMiningBalance, 8)} ${currentMiningCurrency} Gegenwert).` - : `Crypto-Angebote sind aktiv. Standardmäßig werden Laufzeiten ab ${fmtNumber(minOfferRuntimeMonths, 0)} Monaten berücksichtigt. Solange kein Wallet-Bestand erkannt wird, bleiben die Krypto-Angebote sichtbar.`) + ? `Crypto-Angebote sind aktiv. Standardmäßig werden Laufzeiten ab ${fmtNumber(minOfferRuntimeMonths, 0)} Monaten berücksichtigt. Der 50-kH/s-Server mit 36 Monaten bleibt als Referenz immer sichtbar; der Wallet-Bestand begrenzt weder Anzeige noch Mieten.` : `Fiat-Angebote sind aktiv. Standardmäßig werden Laufzeiten ab ${fmtNumber(minOfferRuntimeMonths, 0)} Monaten berücksichtigt. Die Berechnung startet erst, wenn du eine Geschwindigkeit eingibst.`, [ h('div', { key: 'filters', className: 'mc-filter-grid' }, [ @@ -3917,9 +3842,14 @@ h('table', { key: 'table', className: 'mc-table' }, [ h('thead', { key: 'head' }, h('tr', null, ['Label', 'Hashrate', 'Preis', 'Erwartet/Tag', 'Break-even', 'Empfehlung', 'Aktion'].map((label) => h('th', { key: label }, label)))), h('tbody', { key: 'body' }, - filteredMinerOffers.length - ? filteredMinerOffers.map((offer) => h('tr', { key: offer.id }, [ - h('td', { key: 'label' }, offer.label), + visibleMinerOffers.length + ? visibleMinerOffers.map((offer) => h('tr', { key: offer.id }, [ + h('td', { key: 'label' }, [ + h('div', { key: 'name' }, offer.label), + offer.is_pinned_reference + ? h('div', { key: 'reference', className: 'mc-kicker' }, 'Referenzangebot · 50 kH/s · 36 Monate') + : null, + ]), h('td', { key: 'hashrate' }, [ h('div', { key: 'base' }, formatSpeed(offer.mining_speed_value, offer.mining_speed_unit, 'Basis') || 'n/a'), Number(offer.bonus_speed_value) > 0 @@ -3982,7 +3912,6 @@ ? h('div', { key: 'rec-ref', className: 'mc-kicker' }, `Basis ${fmtNumber(offer.base_price_amount, 6)} ${offer.base_price_currency}`) : null, offer.payment_type ? h('div', { key: 'paytype', className: 'mc-kicker' }, offer.payment_type === 'crypto' ? `Zahlung in Krypto (${currentSettings.crypto_currency || 'DOGE'})` : `Zahlung in FIAT (${offer.base_price_currency || 'EUR'})`) : null, - h('div', { key: 'availability', className: 'mc-kicker' }, isOfferAvailableForWallet(offer, latestWalletSnapshot) ? 'Im NC Wallet verfuegbar' : 'Nicht genug NC Wallet Bestand'), h('div', { key: 'renew', className: 'mc-kicker' }, offer.auto_renew ? 'Automatische Verlängerung' : 'Laeuft aus'), ]), h('td', { key: 'action' }, [ @@ -4030,7 +3959,7 @@ }); setPurchaseMinerModalOpen(true); }, - disabled: !isOfferAvailableForWallet(offer, latestWalletSnapshot), + disabled: saving, }, 'Mieten'), ]), ])) diff --git a/custom/apps/mining-checker/docs/README.md b/custom/apps/mining-checker/docs/README.md index 91c1248a..36d8d4df 100644 --- a/custom/apps/mining-checker/docs/README.md +++ b/custom/apps/mining-checker/docs/README.md @@ -6,6 +6,8 @@ Das Modul erfasst Mining-Messpunkte der aktuell konfigurierten Kryptowaehrung, a Im Startseiten-Ueberblick zeigt die Kachel `DOGE Bestand` den direkten Wallet-Bestand, den nach Transfers bereinigten Miner-Bestand, deren Summe sowie den letzten Coin/USD-Kurs. Bei einer geaenderten Mining-Waehrung werden Beschriftung und Werte entsprechend dieser Waehrung gefuehrt. +Der Kursverlauf verwendet pro Upload dessen gespeicherten Kurs und zugeordnete historische FX-Referenz. Uploads ohne Kurs werden nicht als Kurs `0` dargestellt. + ## Ordnerstruktur ```text @@ -120,6 +122,8 @@ Fuer Auswertungen, Berichte und Listen speichert der Mining-Checker pro Messpunk Die Angebotsberechnung fuer Krypto-Miner nutzt eine konfigurierbare Basis fuer `50 kH/s` und `3 Monate`. Standard ist `5.49 USD`, kann aber in den Modul-Settings angepasst werden. +Unabhaengig von aktiven Angebots-, Wallet-, Preis- oder Laufzeitfiltern bleibt der Krypto-Server mit `50 kH/s` und `36 Monaten` als Referenzangebot in der Miner-Angebotsliste sichtbar. Die Zielminer-Kachel auf der Startseite verwendet denselben Angebotsdatensatz und dessen direkt berechneten Krypto-Preis. Der Walletbestand begrenzt weder Angebotsanzeige noch die Aktion `Mieten`, weil der Mietbetrag vor dem Speichern manuell angepasst werden kann. + Zusaetzlich koennen in den Settings diese Leitwerte hinterlegt werden: - minimale gewuenschte Mietlaufzeit in Monaten