Server
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user