Server
All checks were successful
Deploy / deploy-staging (push) Successful in 25s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-08-14 01:08:17 +02:00
parent 9f6368583d
commit 7e8b7f336b
3 changed files with 201 additions and 59 deletions

View File

@@ -1129,26 +1129,71 @@
const selectableFiatCurrencies = preferredSelectableFiatCurrencies.length ? preferredSelectableFiatCurrencies : fiatCurrencies; const selectableFiatCurrencies = preferredSelectableFiatCurrencies.length ? preferredSelectableFiatCurrencies : fiatCurrencies;
const selectableCryptoCurrencies = preferredSelectableCryptoCurrencies.length ? preferredSelectableCryptoCurrencies : cryptoCurrencies; const selectableCryptoCurrencies = preferredSelectableCryptoCurrencies.length ? preferredSelectableCryptoCurrencies : cryptoCurrencies;
const selectedMinerScenario = scenarioMinerOffers.find((offer) => String(offer.id) === String(selectedMinerScenarioId)) || null; const selectedMinerScenario = scenarioMinerOffers.find((offer) => String(offer.id) === String(selectedMinerScenarioId)) || null;
const activeMinerRows = currentPurchasedMiners.map((miner) => { const allMinerRows = currentPurchasedMiners.map((miner) => {
const coverage = entryCoverageMeta(miner.purchased_at, miner.runtime_months, miner.auto_renew);
const effectiveCurrency = String(miner.currency || '').toUpperCase(); 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 { return {
id: `purchase-${miner.id}`, id: `purchase-${miner.id}`,
source: 'miete', source: 'miete',
starts_at: miner.purchased_at, starts_at: miner.purchased_at,
label: miner.label, label: miner.label,
runtime_months: miner.runtime_months, runtime_months: miner.runtime_months,
auto_renew: !!miner.auto_renew, auto_renew: effectiveAutoRenew,
effective_amount: miner.total_cost_amount, effective_amount: miner.total_cost_amount,
effective_currency: miner.currency, effective_currency: miner.currency,
base_amount: miner.reference_price_amount, base_amount: baseAmount,
base_currency: miner.reference_price_currency, base_currency: baseCurrency,
miner_id: miner.id, miner_id: miner.id,
miner_offer_id: miner.miner_offer_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, is_active: miner.is_active !== false && coverage.isCovered,
end_at: coverage.endAt, 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_resource: 'purchased-miners',
toggle_id: miner.id, toggle_id: miner.id,
hashrate_text: formatHashrateWithBonus(miner.mining_speed_value, miner.mining_speed_unit, miner.bonus_speed_value, miner.bonus_speed_unit), 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', type_label: 'Manuell eingetragen',
}; };
})).sort((left, right) => String(right.starts_at || '').localeCompare(String(left.starts_at || ''))); })).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(() => { useEffect(() => {
if (reportCurrencyOverride) { if (reportCurrencyOverride) {
@@ -2745,8 +2792,8 @@
const totalEarnedOverall = latest && latest.growth_since_baseline !== null && latest.growth_since_baseline !== undefined const totalEarnedOverall = latest && latest.growth_since_baseline !== null && latest.growth_since_baseline !== undefined
? Number(latest.growth_since_baseline) ? Number(latest.growth_since_baseline)
: null; : null;
const totalEarnedOverallFiat = latest && totalEarnedOverall !== null const totalEarnedOverallFiat = totalEarnedOverall !== null
? convertMeasurementMoney(latest, totalEarnedOverall, reportCurrency) ? convertCurrencyValue(totalEarnedOverall, currentCoinCurrency, reportCurrency)
: null; : null;
const totalSpentOverallCurrentAsset = latest const totalSpentOverallCurrentAsset = latest
? convertMeasurementMoney( ? convertMeasurementMoney(
@@ -2755,8 +2802,8 @@
currentCoinCurrency currentCoinCurrency
) )
: null; : null;
const totalSpentOverallFiat = latest && totalSpentOverallCurrentAsset !== null const totalSpentOverallFiat = totalSpentOverallCurrentAsset !== null
? convertMeasurementMoney(latest, totalSpentOverallCurrentAsset, reportCurrency) ? convertCurrencyValue(totalSpentOverallCurrentAsset, currentCoinCurrency, reportCurrency)
: null; : null;
const spentByType = activeMinerRows.reduce((summary, row) => { const spentByType = activeMinerRows.reduce((summary, row) => {
const amount = Number(row?.effective_amount); const amount = Number(row?.effective_amount);
@@ -3206,6 +3253,51 @@
const scenarioInvestedCapital = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_invested_capital, reportCurrency) : null; const scenarioInvestedCapital = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_invested_capital, reportCurrency) : null;
const scenarioOfferCost = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_offer_cost, 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 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' }, [ return h('div', { className: 'mc-stack' }, [
panel('Aktive Miner', 'Alle bereits gemieteten oder manuell eingetragenen Miner in einer gemeinsamen Liste.', [ panel('Aktive Miner', 'Alle bereits gemieteten oder manuell eingetragenen Miner in einer gemeinsamen Liste.', [
h('div', { key: 'actions', className: 'mc-inline-row' }, [ h('div', { key: 'actions', className: 'mc-inline-row' }, [
@@ -3240,49 +3332,10 @@
disabled: !scenarioMinerOffers.length, disabled: !scenarioMinerOffers.length,
}, 'Neuen Miner mieten'), }, 'Neuen Miner mieten'),
]), ]),
h('div', { key: 'list', className: 'mc-table-shell' }, [ renderMinerTable(activeMinerRows, 'Noch keine aktiven Miner hinterlegt.'),
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)))), panel('Inaktive Miner', 'Abgelaufene oder deaktivierte Miner werden separat gefuehrt.', [
h('tbody', { key: 'body' }, renderMinerTable(inactiveMinerRows, 'Noch keine inaktiven Miner hinterlegt.'),
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.'))]
),
]),
]),
]), ]),
panel('Miner-Angebote', selectedOfferType === 'crypto' panel('Miner-Angebote', selectedOfferType === 'crypto'
? (currentWalletMiningBalance !== null ? (currentWalletMiningBalance !== null

View File

@@ -1649,7 +1649,7 @@ final class Router
private function costPlans(string $projectKey): array 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 private function payouts(string $projectKey): array
@@ -1770,7 +1770,7 @@ final class Router
private function purchasedMiners(string $projectKey): array 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 private function saveTarget(string $projectKey, array $input): array
@@ -2392,6 +2392,48 @@ final class Router
return $unit; 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 private function requiredPositiveInt(mixed $value, string $field): int
{ {
if ($value === null || $value === '' || !is_numeric((string) $value)) { if ($value === null || $value === '' || !is_numeric((string) $value)) {

View File

@@ -67,10 +67,10 @@ final class AnalyticsService
$fullLatestOnly = !empty($options['full_latest_only']); $fullLatestOnly = !empty($options['full_latest_only']);
$baselineCoins = (float) ($settings['baseline_coins_total'] ?? 0.0); $baselineCoins = (float) ($settings['baseline_coins_total'] ?? 0.0);
$baselineAt = (string) ($settings['baseline_measured_at'] ?? ''); $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'] : []; $payouts = is_array($settings['payouts'] ?? null) ? $settings['payouts'] : [];
$walletWithdrawals = is_array($settings['wallet_withdrawals'] ?? null) ? $settings['wallet_withdrawals'] : []; $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([ $preferredPriceCurrencies = array_values(array_unique(array_filter([
strtoupper(trim((string) ($settings['report_currency'] ?? ''))), strtoupper(trim((string) ($settings['report_currency'] ?? ''))),
'USD', 'USD',
@@ -319,7 +319,7 @@ final class AnalyticsService
$payouts = is_array($settings['payouts'] ?? null) ? $settings['payouts'] : []; $payouts = is_array($settings['payouts'] ?? null) ? $settings['payouts'] : [];
$walletWithdrawals = is_array($settings['wallet_withdrawals'] ?? null) ? $settings['wallet_withdrawals'] : []; $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'] : []; $minerOffers = is_array($settings['miner_offers'] ?? null) ? $settings['miner_offers'] : [];
$currentHashrateMh = $this->totalHashrateMh(array_merge( $currentHashrateMh = $this->totalHashrateMh(array_merge(
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [], 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 private function expandOfferVariants(array $offers, array $settings): array
{ {
$expanded = []; $expanded = [];